<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Building GNOME Apps with Rust on From The Architect</title><link>https://fromthearchitect.dev/series/building-gnome-apps-with-rust/</link><description>Recent content in Building GNOME Apps with Rust on From The Architect</description><image><title>From The Architect</title><url>https://fromthearchitect.dev/og-image.png</url><link>https://fromthearchitect.dev/og-image.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 11 Aug 2026 00:00:00 +1000</lastBuildDate><atom:link href="https://fromthearchitect.dev/series/building-gnome-apps-with-rust/index.xml" rel="self" type="application/rss+xml"/><item><title>Building GNOME Apps with Rust, Part 6: Fetching Feeds</title><link>https://fromthearchitect.dev/posts/gnome-rust-part-6-fetching-feeds/?utm_source=rss</link><pubDate>Tue, 11 Aug 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-part-6-fetching-feeds/</guid><description>Clicking a sidebar row still just logs a line to stderr. Stand up a second executor for Tokio alongside GTK&amp;#39;s single-threaded main loop, fetch and parse real RSS feeds off the UI thread, and learn exactly where the one seam between them is allowed to cross.</description><content:encoded><![CDATA[<p><em>This is Part 6 of a series taking a GNOME app from an empty directory to GNOME Circle. <a href="/posts/gnome-rust-part-5-state-and-signals/">Part 5</a> wired the sidebar to a real <code>Feed</code> GObject and a <code>feed-selected</code> signal — selecting a row updates the content pane and prints a line to the terminal. This is the post where that line stops being a placeholder and becomes a real network fetch.</em></p>
<hr>
<h2 id="the-click-that-still-does-nothing-real">The click that still does nothing real</h2>
<p>Select <em>This Week in GNOME</em>, then select <em>Hacker News</em>. The placeholder swaps in the new name and URL — everything the app knows about that feed, and none of what&rsquo;s actually in it. Every <code>Feed</code> in the sidebar has a <code>uri</code> pointing at a real RSS or Atom document on the actual internet, and nothing in the codebase has ever asked one of them what&rsquo;s there.</p>
<p>The obvious fix — fetch the URL when <code>feed-selected</code> fires — is one line of code and the wrong line of code. GTK doesn&rsquo;t have a spare thread lying around to do that on, and the thread it does have is busy.</p>
<p>By the end of this post, selecting <em>Hacker News</em> in the sidebar prints real headlines to the terminal, fetched over the network, without the window so much as flickering. We&rsquo;ll build a Tokio runtime alongside GTK&rsquo;s own, give <code>Feed</code> somewhere to put what it fetches, and draw a hard line neither executor is allowed to cross.</p>
<hr>
<h2 id="the-freeze">The freeze</h2>
<p>Wire the obvious fix into the <code>feed-selected</code> handler first, so you can watch it fail:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>obj.connect_closure(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;feed-selected&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    glib::<span style="color:#a6e22e">closure_local!</span>(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_window: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">super</span>::GazetteWindow, feed: <span style="color:#a6e22e">Feed</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Don&#39;t do this.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        std::thread::sleep(std::time::Duration::from_secs(<span style="color:#ae81ff">2</span>));
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">eprintln!</span>(<span style="color:#e6db74">&#34;fetched (fake): </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, feed.name());
</span></span><span style="display:flex;"><span>    }),
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p><code>std::thread::sleep</code> stands in for &ldquo;network request in flight.&rdquo; Run the app and click a feed. For two seconds the window stops responding to anything — no hover states, no resize, no cursor blink, nothing repaints. Click a different feed while the first is still &ldquo;fetching&rdquo; and the click doesn&rsquo;t register until the sleep ends; it queues up invisibly and the window only notices you clicked at all once the first one finishes.</p>
<p>If you have GTK Inspector open (<code>GTK_DEBUG=interactive</code>, or Ctrl+Shift+I) while you try this, its own live views stall for the same two seconds — Inspector needs the app&rsquo;s main loop to be responsive to answer its own queries, so a frozen loop is invisible to Inspector too. Worth knowing the next time you&rsquo;re not sure whether <em>your</em> code froze or something else did.</p>
<p>Revert the handler to the logging-only version from Part 5 before continuing. The freeze was the demonstration, not a step you keep.</p>
<hr>
<h2 id="one-main-loop-nothing-else-runs-until-you-give-it-back">One main loop, nothing else runs until you give it back</h2>
<p>GTK schedules everything through a single <code>glib::MainContext</code>, running cooperatively on a single thread: one job finishes before the next one starts. When you clicked a feed a moment ago, the redraw that should have shown the hover state, the cursor update, the eventual repaint once the click registered — all of it queued behind your <code>feed-selected</code> handler, waiting for <code>std::thread::sleep</code> to let go of the only thread any of it can run on.</p>
<p>That&rsquo;s a deliberate tradeoff. Exactly one thread ever touches a widget, so there&rsquo;s no locking, no data races, no <code>Mutex&lt;Widget&gt;</code>. The price is that the one thread has to keep coming back. Block it — a sleep, a synchronous file read, a blocking network call, anything — and every other queued job waits behind you.</p>
<p>This is also why &ldquo;just spawn a thread and do it there&rdquo; isn&rsquo;t a fix, only a different failure. Almost every GTK/GObject type is <code>!Send</code>: the bindings won&rsquo;t even let you move a <code>Feed</code> or a widget handle across a thread boundary — it&rsquo;s a compile error, not a runtime one. GObject&rsquo;s own machinery assumes single-threaded access and doesn&rsquo;t defend against concurrent calls from a second thread. A background thread can compute a result, but it can&rsquo;t hand that result to a widget directly. Something still has to get the result back onto the one thread allowed to touch GTK state.</p>
<hr>
<h2 id="two-executors-one-job-each">Two executors, one job each</h2>
<p>The pattern that resolves this has a name because the shape recurs in every GTK app that talks to a network: two executors, each with a rule it never breaks.</p>
<p>The <strong>GLib main loop</strong> owns every widget, every GObject, every signal, and it must never block.</p>
<p><strong>Tokio</strong> owns anything that can block — the network request here, file I/O or database queries later. It runs on its own thread pool, and it never touches a widget or a GObject directly. That&rsquo;s its rule.</p>
<p>The two only meet at one seam: a future running on the GLib main context can <code>.await</code> a handle to work Tokio is doing, and when that work finishes, execution resumes back on the main context with the result now sitting there as a plain value. Nothing crosses except that value — no <code>Feed</code>, no <code>gtk::Label</code>, nothing GTK owns ever travels to a Tokio thread, and no raw socket or blocking read ever runs on the GLib thread. The rest of this post is standing up both executors and building that one seam.</p>
<hr>
<h2 id="standing-up-the-second-executor">Standing up the second executor</h2>
<p>Add the dependencies:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">tokio</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;1.53.1&#34;</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;rt-multi-thread&#34;</span>, <span style="color:#e6db74">&#34;net&#34;</span>, <span style="color:#e6db74">&#34;time&#34;</span>] }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">feed-rs</span> = <span style="color:#e6db74">&#34;2.4.0&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">reqwest</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.12&#34;</span>, <span style="color:#a6e22e">default-features</span> = <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;rustls-tls&#34;</span>] }
</span></span></code></pre></div><p><code>rt-multi-thread</code> gives the runtime its worker threads; <code>net</code> and <code>time</code> give it the I/O and timer drivers <code>reqwest</code> — and any future <code>tokio::time::sleep</code> — actually needs. That matters more than it looks like it should: <code>reqwest</code> also depends on <code>tokio</code> and pulls in <code>net</code>/<code>time</code> itself, and Cargo unifies features across the whole dependency graph. Leave them off <code>tokio</code>&rsquo;s own line and the project still compiles today, by accident of what else happens to be in <code>Cargo.toml</code>. It breaks the day <code>reqwest</code> changes its feature list, or the day someone copies this dependency line into a project with no <code>reqwest</code> in it.</p>
<p><code>reqwest</code> with <code>default-features = false</code> and <code>rustls-tls</code> pulls in a pure-Rust TLS stack instead of linking against the system&rsquo;s OpenSSL — one less thing the Flatpak manifest needs to account for. The <code>0.12</code> pin is deliberate too: <code>0.13</code> renamed that feature to <code>rustls</code> and made it the default backend, which would leave the line above redundant rather than wrong, but <code>0.12</code> is what this post was written and tested against, and mixing versions is a worse trap than an old pin.</p>
<p>The runtime itself is built once, in <code>main()</code>, before the GTK application exists:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">// The second executor. Built before the GTK application and dropped
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// after it exits, so it outlives every fetch that borrows its handle —
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// the GLib main loop is the other executor, and it never blocks
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// waiting on this one.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">let</span> tokio_runtime <span style="color:#f92672">=</span> tokio::runtime::Builder::new_multi_thread()
</span></span><span style="display:flex;"><span>    .worker_threads(<span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    .thread_name(<span style="color:#e6db74">&#34;gazette-fetch&#34;</span>)
</span></span><span style="display:flex;"><span>    .enable_all()
</span></span><span style="display:flex;"><span>    .build()
</span></span><span style="display:flex;"><span>    .expect(<span style="color:#e6db74">&#34;failed to build Tokio runtime&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> app <span style="color:#f92672">=</span> GazetteApplication::new(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;io.github.fromthearchitect.gazette&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&amp;</span>gio::ApplicationFlags::empty(),
</span></span><span style="display:flex;"><span>    tokio_runtime.handle().clone(),
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> exit_code <span style="color:#f92672">=</span> app.run();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// `tokio_runtime` would drop here anyway at end of scope; the explicit
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// drop just documents the ordering. `Runtime::drop` blocks this thread
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// until every worker stops and abandons whatever tasks haven&#39;t
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// finished — abrupt, not a cooperative cancel — which is still better
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// than a fetch outliving the window that would have consumed it.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>drop(tokio_runtime);
</span></span><span style="display:flex;"><span>exit_code
</span></span></code></pre></div><p><code>app.run()</code> blocks until the GTK application quits — that&rsquo;s the GLib main loop, occupying this thread for the entire life of the app. The Tokio runtime doesn&rsquo;t need this thread; <code>Builder::new_multi_thread</code> spins up its own worker threads (two of them here, named for easy identification in a debugger) and hands back a <code>Runtime</code> you only need to keep alive, not sit inside. <code>tokio_runtime.handle().clone()</code> is the thing that actually travels: a cheap, cloneable reference that can schedule work onto the runtime from any thread, including the GLib main thread.</p>
<p>One trap worth flagging on its own: <strong>forgetting <code>.enable_all()</code> fails silently until the first real request.</strong> A <code>Builder</code> without it produces a runtime with no I/O or timer driver — a different gap from the missing Cargo features above, this one is about whether a correctly-compiled runtime <em>instance</em> turns its drivers on. It builds fine, <code>main()</code> runs fine, the window opens fine, and the first time a spawned task tries to do the thing a runtime is for — <code>reqwest::get</code> or <code>tokio::time::sleep</code> — it panics with a message about no I/O driver running. The bug is invisible until the exact code path that needs the missing driver executes, which is exactly the fetch path this post is building.</p>
<p>That <code>Handle</code> needs a home the rest of the app can reach. <code>GazetteApplication</code> already owns everything else global to a running instance, so it owns this too — the same <code>OnceCell</code> shape Part 5 used for <code>feeds: OnceCell&lt;gio::ListStore&gt;</code> on the window: set once, read forever after.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug, Default)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteApplication</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// The second executor. Built once in `main()`, before the GTK
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// application runs, and outlives every fetch that borrows it.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">pub</span> tokio: <span style="color:#a6e22e">OnceCell</span><span style="color:#f92672">&lt;</span>tokio::runtime::Handle<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> GazetteApplication {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">new</span>(
</span></span><span style="display:flex;"><span>        application_id: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>,
</span></span><span style="display:flex;"><span>        flags: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">gio</span>::ApplicationFlags,
</span></span><span style="display:flex;"><span>        tokio: <span style="color:#a6e22e">tokio</span>::runtime::Handle,
</span></span><span style="display:flex;"><span>    ) -&gt; <span style="color:#a6e22e">Self</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> app: <span style="color:#a6e22e">Self</span> <span style="color:#f92672">=</span> glib::Object::builder()
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;application-id&#34;</span>, application_id)
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;flags&#34;</span>, flags)
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;resource-base-path&#34;</span>, <span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette&#34;</span>)
</span></span><span style="display:flex;"><span>            .build();
</span></span><span style="display:flex;"><span>        app.imp()
</span></span><span style="display:flex;"><span>            .tokio
</span></span><span style="display:flex;"><span>            .set(tokio)
</span></span><span style="display:flex;"><span>            .expect(<span style="color:#e6db74">&#34;tokio handle set once at construction&#34;</span>);
</span></span><span style="display:flex;"><span>        app
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">/// The shared Tokio runtime handle — the executor every network fetch
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>    <span style="color:#e6db74">/// runs on. Available from any `GazetteWindow` via its `application()`.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">tokio_handle</span>(<span style="color:#f92672">&amp;</span>self) -&gt; <span style="color:#a6e22e">tokio</span>::runtime::Handle {
</span></span><span style="display:flex;"><span>        self.imp().tokio.get().expect(<span style="color:#e6db74">&#34;tokio handle set&#34;</span>).clone()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>build()</code> returns a fully constructed object. If <code>GazetteApplication</code> ever grows a <code>constructed()</code> that reaches for <code>tokio_handle()</code>, it will find the cell empty — <code>constructed()</code> runs <em>during</em> <code>build()</code>, before the line that sets <code>tokio</code> ever executes. Today&rsquo;s code escapes that trap by timing rather than by ordering: nothing that needs the handle runs until GTK calls <code>activate</code>, and <code>activate</code> doesn&rsquo;t fire until <code>app.run()</code>, long after <code>new</code> has returned with <code>tokio</code> set. Any window can reach the runtime through <code>window.application()</code>, downcast to <code>GazetteApplication</code>, and call <code>tokio_handle()</code>.</p>
<hr>
<h2 id="fetching-off-the-main-thread">Fetching, off the main thread</h2>
<p>The fetch itself lives in a new file, <code>src/fetch.rs</code>, deliberately separate from <code>feed.rs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::fmt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">/// A single parsed entry from a feed. Plain data — not a GObject. It&#39;s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">/// built on the Tokio runtime and only crosses onto the GLib main context
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">/// as a value, via `Feed::set_items`.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span><span style="color:#75715e">#[derive(Debug, Clone)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">FeedItem</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> title: String,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> link: String,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> summary: Option<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">enum</span> <span style="color:#a6e22e">FetchError</span> {
</span></span><span style="display:flex;"><span>    Request(reqwest::Error),
</span></span><span style="display:flex;"><span>    Parse(feed_rs::parser::ParseFeedError),
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> fmt::Display <span style="color:#66d9ef">for</span> FetchError {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">fmt</span>(<span style="color:#f92672">&amp;</span>self, f: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">mut</span> fmt::Formatter<span style="color:#f92672">&lt;</span>&#39;_<span style="color:#f92672">&gt;</span>) -&gt; <span style="color:#a6e22e">fmt</span>::Result {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">match</span> self {
</span></span><span style="display:flex;"><span>            FetchError::Request(e) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">write!</span>(f, <span style="color:#e6db74">&#34;request failed: {e}&#34;</span>),
</span></span><span style="display:flex;"><span>            FetchError::Parse(e) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">write!</span>(f, <span style="color:#e6db74">&#34;couldn&#39;t parse feed: {e}&#34;</span>),
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">/// Fetches and parses a feed at `uri`. Runs entirely on the Tokio runtime —
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">/// nothing in this function, or anything it calls, touches a widget or a
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">/// GObject. That&#39;s what makes it safe to run off the GLib main thread.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">fetch_feed</span>(uri: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; Result<span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>FeedItem<span style="color:#f92672">&gt;</span>, FetchError<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> response <span style="color:#f92672">=</span> reqwest::get(uri)
</span></span><span style="display:flex;"><span>        .<span style="color:#66d9ef">await</span>
</span></span><span style="display:flex;"><span>        .map_err(FetchError::Request)<span style="color:#f92672">?</span>
</span></span><span style="display:flex;"><span>        .error_for_status()
</span></span><span style="display:flex;"><span>        .map_err(FetchError::Request)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> bytes <span style="color:#f92672">=</span> response.bytes().<span style="color:#66d9ef">await</span>.map_err(FetchError::Request)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> parsed <span style="color:#f92672">=</span> feed_rs::parser::parse(<span style="color:#f92672">&amp;</span>bytes[<span style="color:#f92672">..</span>]).map_err(FetchError::Parse)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Ok(parsed
</span></span><span style="display:flex;"><span>        .entries
</span></span><span style="display:flex;"><span>        .into_iter()
</span></span><span style="display:flex;"><span>        .map(<span style="color:#f92672">|</span>entry<span style="color:#f92672">|</span> FeedItem {
</span></span><span style="display:flex;"><span>            title: <span style="color:#a6e22e">entry</span>
</span></span><span style="display:flex;"><span>                .title
</span></span><span style="display:flex;"><span>                .map(<span style="color:#f92672">|</span>text<span style="color:#f92672">|</span> text.content)
</span></span><span style="display:flex;"><span>                .unwrap_or_else(<span style="color:#f92672">||</span> <span style="color:#e6db74">&#34;Untitled&#34;</span>.to_string()),
</span></span><span style="display:flex;"><span>            link: <span style="color:#a6e22e">entry</span>
</span></span><span style="display:flex;"><span>                .links
</span></span><span style="display:flex;"><span>                .first()
</span></span><span style="display:flex;"><span>                .map(<span style="color:#f92672">|</span>link<span style="color:#f92672">|</span> link.href.clone())
</span></span><span style="display:flex;"><span>                .unwrap_or_default(),
</span></span><span style="display:flex;"><span>            summary: <span style="color:#a6e22e">entry</span>.summary.map(<span style="color:#f92672">|</span>text<span style="color:#f92672">|</span> text.content),
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        .collect())
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>FeedItem</code> is a plain struct rather than a <code>glib::wrapper!</code> type, and that matters: constructing a GObject means calling into GObject&rsquo;s type system, and this function has no business doing that from a Tokio worker thread. <code>fetch_feed</code> produces data; something back on the main thread decides what GObject, if any, that data becomes.</p>
<p><code>error_for_status()</code> matters more than it looks. Without it, <code>reqwest::get</code> resolves <code>Ok</code> for a 404, a 500, or a hosting provider&rsquo;s error interstitial — the HTTP request itself succeeded even though the content is a lie. Skip the check and <code>feed_rs::parser::parse</code> gets handed an HTML error page instead of a feed, fails to parse it, and the caller sees <code>FetchError::Parse</code> for what was actually a request-level failure. Checking status before reading the body is what keeps <code>Request</code> meaning &ldquo;didn&rsquo;t get a real feed body at all&rdquo; — whether the cause is DNS, a timeout, or a 404 — and <code>Parse</code> meaning &ldquo;got a body and it wasn&rsquo;t a feed.&rdquo;</p>
<p><code>feed_rs::parser::parse</code> takes anything implementing <code>std::io::Read</code> (a byte slice qualifies) and returns entries regardless of whether the source was RSS or Atom. Gazette&rsquo;s four sample feeds are a mix of both — <em>This Week in GNOME</em> and <em>From the Architect</em> are Atom, <em>LWN</em> and the Hacker News mirror are RSS — and <code>fetch_feed</code> doesn&rsquo;t need to know or care which. That format detection is <code>feed-rs</code>&rsquo;s job, done once, before any of Gazette&rsquo;s code sees an entry.</p>
<p>One thing worth knowing, not fixing here: <code>reqwest::get</code> is a convenience function that builds a fresh <code>Client</code> — and with it, a fresh connection pool and TLS session — on every call. Fine for a post about the boundary between two executors, wasteful in code that fetches the same handful of feeds over and over. Building a <code>Client</code> once and sharing it alongside the Tokio handle is a refinement for a later post, once persistence is in the picture.</p>
<hr>
<h2 id="crossing-back">Crossing back</h2>
<p>The <code>feed-selected</code> handler is where the two executors actually meet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>obj.connect_closure(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;feed-selected&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    glib::<span style="color:#a6e22e">closure_local!</span>(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>window: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">super</span>::GazetteWindow, feed: <span style="color:#a6e22e">Feed</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">eprintln!</span>(<span style="color:#e6db74">&#34;feed selected: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74"> (</span><span style="color:#e6db74">{}</span><span style="color:#e6db74">)&#34;</span>, feed.name(), feed.uri());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// The GLib executor owns `feed` and `window`; the Tokio
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// executor (below) only ever sees the URI string it needs
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// to fetch. Neither side touches the other&#39;s objects
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// directly — the `.await` on the join handle is the only
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// crossing.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">let</span> tokio_handle <span style="color:#f92672">=</span> window
</span></span><span style="display:flex;"><span>            .application()
</span></span><span style="display:flex;"><span>            .and_downcast::<span style="color:#f92672">&lt;</span><span style="color:#66d9ef">crate</span>::application::GazetteApplication<span style="color:#f92672">&gt;</span>()
</span></span><span style="display:flex;"><span>            .expect(<span style="color:#e6db74">&#34;window has a GazetteApplication&#34;</span>)
</span></span><span style="display:flex;"><span>            .tokio_handle();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> uri <span style="color:#f92672">=</span> feed.uri();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        glib::MainContext::default().spawn_local(<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">move</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> result <span style="color:#f92672">=</span> tokio_handle
</span></span><span style="display:flex;"><span>                .spawn(<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">move</span> { <span style="color:#66d9ef">crate</span>::fetch::fetch_feed(<span style="color:#f92672">&amp;</span>uri).<span style="color:#66d9ef">await</span> })
</span></span><span style="display:flex;"><span>                .<span style="color:#66d9ef">await</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">match</span> result {
</span></span><span style="display:flex;"><span>                Ok(Ok(items)) <span style="color:#f92672">=&gt;</span> feed.set_items(items),
</span></span><span style="display:flex;"><span>                Ok(Err(fetch_err)) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">eprintln!</span>(<span style="color:#e6db74">&#34;fetch failed for </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{fetch_err}</span><span style="color:#e6db74">&#34;</span>, feed.name());
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                Err(join_err) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">eprintln!</span>(
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#34;fetch task for </span><span style="color:#e6db74">{}</span><span style="color:#e6db74"> did not complete: </span><span style="color:#e6db74">{join_err}</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                        feed.name()
</span></span><span style="display:flex;"><span>                    );
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        });
</span></span><span style="display:flex;"><span>    }),
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Read it from the outside in. <code>glib::MainContext::default().spawn_local(async move { ... })</code> schedules the outer future on the GLib main context — the same executor that owns every widget — running it cooperatively alongside everything else, exactly like a signal handler. Because it&rsquo;s confined to the main context, it&rsquo;s allowed to hold <code>feed</code> and eventually call <code>feed.set_items(...)</code>. That&rsquo;s still executor-one territory.</p>
<p>Inside it, <code>tokio_handle.spawn(async move { crate::fetch::fetch_feed(&amp;uri).await })</code> hands the actual work to executor two. <code>Handle::spawn</code> returns immediately with a <code>JoinHandle</code>; it doesn&rsquo;t block the caller, which is the entire point of calling it from the main thread. <code>.await</code>ing that <code>JoinHandle</code> is where the outer future pauses: control genuinely returns to the GLib main loop, which goes on painting, handling input, and dispatching other signals while Tokio&rsquo;s worker threads do the fetch. When the Tokio task finishes, the main loop resumes this specific future exactly where it left off, back on the main thread, with <code>result</code> now holding a value.</p>
<p>Why <code>Ok(Ok(items))</code> and not just <code>Ok(items)</code>? Two different things can go wrong, and they&rsquo;re wrapped separately. The outer <code>Result</code> comes from <code>JoinHandle</code> itself — its <code>Err</code> means the spawned task panicked or was cancelled, a Tokio-level failure that has nothing to do with feeds. The inner <code>Result</code> is <code>fetch_feed</code>&rsquo;s own <code>Result&lt;Vec&lt;FeedItem&gt;, FetchError&gt;</code>, a normal, expected outcome of trying to fetch something over a network that might be down. Matching both layers explicitly is what forces you to decide what each one means, rather than letting <code>?</code> collapse them into a single generic failure.</p>
<p>And a specific footgun: <strong><code>tokio::spawn(...)</code>, the free function, panics here — <code>tokio_handle.spawn(...)</code> doesn&rsquo;t.</strong> The free function only works from <em>inside</em> a future that&rsquo;s already running on a Tokio runtime. Called from the GLib main thread, which isn&rsquo;t inside any Tokio runtime, it panics; the exact wording has changed across Tokio versions, but it always amounts to &ldquo;no runtime found in this context.&rdquo; <code>Handle::spawn</code>, called on the <code>Handle</code> cloned out in <code>main()</code>, is explicitly designed to work from any thread, runtime or not — that&rsquo;s the entire reason <code>GazetteApplication</code> stores a <code>Handle</code> instead of a <code>Runtime</code>. If you ever see that panic, you&rsquo;ve reached for the free function where you needed the handle.</p>
<p>Run the app now. Click <em>Hacker News</em>: after a brief pause — a real network round trip, with the window still live under your cursor — real headlines appear on stderr. Click <em>LWN.net Headlines</em> before the first fetch has printed anything, and both proceed independently.</p>
<hr>
<h2 id="feed-remembers-what-it-fetched">Feed remembers what it fetched</h2>
<p><code>set_items</code> is doing real work in that handler, and <code>Feed</code> needs somewhere to put what it&rsquo;s given:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#66d9ef">crate</span>::fetch::FeedItem;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug, Default, Properties)]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[properties(wrapper_type = super::Feed)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Feed</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> name: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> uri: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> unread_count: <span style="color:#a6e22e">Cell</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">u32</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Plain data, not a property: `Vec&lt;FeedItem&gt;` has no `glib::Value`
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// mapping, and nothing outside this module needs to bind to it
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// directly. `items()` and `set_items()` are the accessors.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">pub</span> items: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>FeedItem<span style="color:#f92672">&gt;&gt;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> Feed {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">items</span>(<span style="color:#f92672">&amp;</span>self) -&gt; Vec<span style="color:#f92672">&lt;</span>FeedItem<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>        self.imp().items.borrow().clone()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">/// Replaces this feed&#39;s items and emits `items-updated`. The count
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>    <span style="color:#e6db74">/// crossing over from a real fetch is what makes `unread-count` real
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>    <span style="color:#e6db74">/// too, rather than the placeholder zero every sample feed starts with.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">set_items</span>(<span style="color:#f92672">&amp;</span>self, items: Vec<span style="color:#f92672">&lt;</span>FeedItem<span style="color:#f92672">&gt;</span>) {
</span></span><span style="display:flex;"><span>        self.set_unread_count(items.len() <span style="color:#66d9ef">as</span> <span style="color:#66d9ef">u32</span>);
</span></span><span style="display:flex;"><span>        self.imp().items.replace(items);
</span></span><span style="display:flex;"><span>        self.emit_by_name::<span style="color:#f92672">&lt;</span>()<span style="color:#f92672">&gt;</span>(<span style="color:#e6db74">&#34;items-updated&#34;</span>, <span style="color:#f92672">&amp;</span>[]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>items</code> isn&rsquo;t a <code>#[property]</code>: a <code>Vec&lt;FeedItem&gt;</code> has no <code>glib::Value</code> representation the way a <code>String</code> or a <code>u32</code> does, and nothing outside <code>Feed</code> needs to bind a widget to the raw list. It&rsquo;s a <code>RefCell</code> for the same reason <code>selected_feed</code> was one back in Part 5 — a value that changes over the object&rsquo;s life, read from one place (<code>items()</code>), written from one place (<code>set_items()</code>).</p>
<p>The <code>items-updated</code> signal was declared back in Part 2, on a <code>Feed</code> that never actually got new items, then Part 5 dropped it entirely to avoid emitting ceremony for nothing. It comes back here on the same <code>impl</code> block that already derives <code>Feed</code>&rsquo;s properties — with one change from Part 2&rsquo;s sketch:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[glib::derived_properties]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> Feed {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">signals</span>() -&gt; <span style="color:#66d9ef">&amp;</span>&#39;static [Signal] {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">SIGNALS</span>: <span style="color:#a6e22e">OnceLock</span><span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>Signal<span style="color:#f92672">&gt;&gt;</span> <span style="color:#f92672">=</span> OnceLock::new();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">SIGNALS</span>.get_or_init(<span style="color:#f92672">||</span> <span style="color:#a6e22e">vec!</span>[Signal::builder(<span style="color:#e6db74">&#34;items-updated&#34;</span>).build()])
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Part 2&rsquo;s version carried a <code>u32</code> payload — the new item count, passed directly on the signal. This one carries nothing. <code>Feed</code> has an <code>unread-count</code> property now, so a listener that wants the count can just read <code>feed.unread_count()</code> instead of catching it off the emission. A signal that announces <em>something changed</em> and lets listeners go look is easier to evolve than one that ships a payload every listener has to keep accepting — that&rsquo;s the shape the rest of this post builds on.</p>
<p><code>#[glib::derived_properties]</code> has to stay on this <code>impl</code>, even though <code>signals()</code> has nothing to do with properties: it&rsquo;s what generates <code>properties()</code>, <code>property()</code>, and <code>set_property()</code> from the <code>#[property]</code> fields on the struct above. Drop it while adding <code>signals()</code> and every <code>#[property]</code> accessor <code>Feed</code> already has — <code>name()</code>, <code>uri()</code>, <code>set_unread_count()</code> — silently stops working.</p>
<p>What&rsquo;s different now is that <code>set_items</code> is the only thing that ever emits <code>items-updated</code>, and it&rsquo;s only ever called with data that actually came off the network. <code>set_unread_count</code> runs first, before the emit, and the count that shows up in the sidebar is the number of items this fetch found.</p>
<p>That&rsquo;s a narrower claim than &ldquo;unread,&rdquo; on purpose. Every fetch sets <code>unread-count</code> to the <em>total</em> item count, not the count of items the user hasn&rsquo;t seen. Refetch a feed after reading everything in it and the badge goes straight back up to the full number. <code>unread_count</code> is standing in for a property Gazette doesn&rsquo;t have the machinery to compute correctly yet — real read-tracking needs per-item state, which doesn&rsquo;t exist until this series adds persistence. Until then, read the name as aspirational, not as a bug you need to chase.</p>
<hr>
<h2 id="a-consumer-for-items-updated">A consumer for items-updated</h2>
<p><code>set_items</code> emits; something has to be listening. Each sample feed gets a logging handler when the store is built, in <code>constructed()</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> store <span style="color:#f92672">=</span> gio::ListStore::new::<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (name, uri) <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">SAMPLE_FEEDS</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> feed <span style="color:#f92672">=</span> Feed::new(name, uri);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Reacting to items-updated is a separate job from fetching
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// them — the fetch (wired below, on feed-selected) never
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// logs directly, it just calls `feed.set_items(...)`.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    feed.connect_closure(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;items-updated&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>        glib::<span style="color:#a6e22e">closure_local!</span>(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>feed: <span style="color:#a6e22e">Feed</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> items <span style="color:#f92672">=</span> feed.items();
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">eprintln!</span>(<span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74"> items&#34;</span>, feed.name(), items.len());
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> item <span style="color:#66d9ef">in</span> items.iter().take(<span style="color:#ae81ff">3</span>) {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">eprintln!</span>(<span style="color:#e6db74">&#34;  - </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, item.title);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }),
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    store.append(<span style="color:#f92672">&amp;</span>feed);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notice the fetch code in <code>feed-selected</code> never calls <code>eprintln!</code> on success. It just calls <code>feed.set_items(items)</code> and stops. Printing what arrived is this closure&rsquo;s job, connected once per feed, entirely separate from the code that went and got the data. That&rsquo;s the same lesson Part 5 landed with the placeholder binding and the <code>feed-selected</code> logger: fetching is one job, reacting to what was fetched is another, and the signal between them is what keeps neither one needing to know the other exists. A future persistence layer connects its own handler to <code>items-updated</code> later without touching a line of <code>fetch.rs</code>.</p>
<hr>
<h2 id="sharp-edges">Sharp edges</h2>
<p>A <code>RefCell</code> borrow held across an <code>.await</code> fails the same way a borrow held across a signal emit did in Part 5, just harder to spot. The failure shape: <code>let items = self.items.borrow_mut(); some_future.await;</code> inside a <code>spawn_local</code> block, where the code resumed after the <code>.await</code> — or a handler running on a different turn of the main loop, if the borrow escapes further — tries to borrow the same cell. Signal handlers run synchronously, so a Part 5-style borrow panic happens right where you&rsquo;d look for it. An <code>.await</code> point hands control back to the main loop for an arbitrary amount of time: anything else the loop dispatches in between, including another handler that reaches for the same <code>RefCell</code>, can trigger the panic somewhere that looks nothing like the code that caused it. Keep borrows scoped tightly, and never hold one across an <code>.await</code>.</p>
<p>What happens if you click the same feed five times in a row? Five independent fetches run concurrently, each racing to call <code>set_items</code> on the same <code>Feed</code> when it finishes — last one to land wins, and <code>items-updated</code> fires five times for one selection. Harmless for a post about the executor boundary, but worth knowing before you go adding a refresh action, which makes hammering that shortcut the obvious next thing to try. A real fix would track the in-flight <code>JoinHandle</code> on <code>Feed</code> and abort or ignore a new request while one&rsquo;s outstanding — outside this post&rsquo;s scope, but that&rsquo;s where the fix would go.</p>
<p><code>window.application()</code> returns <code>None</code> before the window has one, and <code>expect</code> turns that into a panic. In practice this never fires here: <code>GazetteWindow::new</code> always takes an application, and nothing runs <code>feed-selected</code> before the window exists. The <code>expect</code> documents an invariant rather than checking one — this window always has an application by the time it can receive input. If you ever construct a <code>GazetteWindow</code> without one, a test most likely, this is where it breaks.</p>
<hr>
<h2 id="what-we-have-so-far">What we have so far</h2>
<p>Selecting a feed now does real work. A Tokio runtime, built once alongside the GTK application and never touched by it directly, does the actual fetching and parsing, and the only thing that ever crosses back onto the GLib main thread is a <code>Result</code> full of plain data. <code>Feed::items-updated</code> fires for real for the first time since Part 2 declared it, driven by whatever <code>feed-rs</code> found at the far end of a real URL.</p>
<p>Nothing about this displays anywhere yet — the items live in a <code>RefCell</code> a logging handler reads, and the sidebar still shows nothing but a name and a count. The fetch mechanism this post built doesn&rsquo;t need to change at all to support that; it already hands off a <code>Vec&lt;FeedItem&gt;</code> to whatever wants to render it.</p>
<hr>
<h2 id="what-comes-next">What comes next</h2>
<p>That&rsquo;s the gap the next post closes: an article list for the selected feed, a content pane for the selected article, and the adaptive layout that collapses both into a single pane on a narrow window. The two-executor pattern from this post doesn&rsquo;t change to support any of it.</p>
<p>Further out, <code>unread_count</code> and <code>items</code> want somewhere durable to live, so a refetch doesn&rsquo;t erase read state and a restart doesn&rsquo;t lose it either. That&rsquo;s the persistence layer this series has been setting up for since the <code>OnceCell</code> and <code>RefCell</code> conventions in Part 5 — a real place for <code>items-updated</code> to write to, instead of a logging handler standing in for one.</p>
<hr>
<p>The source code at the end of this post lives on the <code>part-6</code> branch of <a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-6" target="_blank" rel="noopener">fromthearchitect/gnome-rust-gazette</a>.</p>
]]></content:encoded></item><item><title>Building GNOME Apps with Rust, Part 5: State and Signals</title><link>https://fromthearchitect.dev/posts/gnome-rust-part-5-state-and-signals/?utm_source=rss</link><pubDate>Tue, 02 Jun 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-part-5-state-and-signals/</guid><description>Clicking a sidebar row finally does something. Build a real Feed GObject, bind a list to a model, define a custom signal, and learn where state lives in a GTK Rust app — and why Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt; almost never belongs there.</description><content:encoded><![CDATA[<p><em>This is Part 5 of a series taking a GNOME app from an empty directory to GNOME Circle. <a href="/posts/gnome-rust-part-4-blueprint/">Part 4</a> replaced our XML templates with Blueprint and grew the window into the real Gazette layout — a sidebar, a content pane, and three typed widget handles waiting for behaviour. This is the post where they get some.</em></p>
<p><em>If the GObject machinery in here feels unfamiliar — <code>mod imp</code>, properties, signals — <a href="/posts/gnome-rust-part-2-gobject/">Part 2</a> is the reference. This is where those patterns stop being theoretical.</em></p>
<hr>
<h2 id="the-click-that-goes-nowhere">The click that goes nowhere</h2>
<p>Right now, clicking a sidebar row does nothing. GTK draws a selection highlight, the row picks up its hover state, and that&rsquo;s it — our code never sees the click. The whole point of the sidebar is that it drives the content pane, and we haven&rsquo;t wired that yet.</p>
<p>By the end of this post, selecting a row drives everything to its right. The shape I keep coming back to in GTK apps is a chain — model to list to selection to signal to content — and getting those links in the right order is half the work. The other half is interior mutability: where state lives, when <code>RefCell</code> earns its place, when <code>OnceCell</code> does, and why the <code>Rc&lt;RefCell&lt;T&gt;&gt;</code> reflex most of us carry over from non-GUI Rust almost never belongs in a <code>mod imp</code>-shaped app. I&rsquo;ll take each one where it comes up rather than in the abstract.</p>
<p>Concretely, we&rsquo;ll build a real <code>Feed</code> GObject with <code>name</code> and <code>uri</code> properties, swap that frozen sidebar row for a <code>gio::ListStore&lt;Feed&gt;</code> seeded with four real feeds, define a custom <code>feed-selected(Feed)</code> signal on the window, and make the content pane react — feed name as the title, URL as the description, the empty-state placeholder showing only when nothing&rsquo;s selected. That <code>feed-selected</code> signal is the seam Part 6 plugs Tokio into: the synchronous handler we write here gets joined by an asynchronous fetch handler that lives entirely in another file, and designing for that boundary now is the quieter lesson of the post.</p>
<hr>
<h2 id="feed-for-real">Feed, for real</h2>
<p>Part 2 sketched a <code>Feed</code> GObject — a title, an <code>items-updated</code> signal, the whole <code>mod imp</code> apparatus. None of it landed in source. We got away with it because Parts 3 through 4 only needed widgets, not data. That ends here. Before we can have a sidebar that <em>means</em> something, we need a model for what the sidebar contains.</p>
<p>Create <code>src/feed.rs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::glib;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::glib::Properties;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::subclass::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::cell::RefCell;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> imp {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">super</span>::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[derive(Debug, Default, Properties)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[properties(wrapper_type = super::Feed)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Feed</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> name: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> uri: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[glib::object_subclass]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectSubclass <span style="color:#66d9ef">for</span> Feed {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">NAME</span>: <span style="color:#66d9ef">&amp;</span>&#39;static <span style="color:#66d9ef">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;GazetteFeed&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Type</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">super</span>::Feed;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[glib::derived_properties]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> Feed {}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>glib::<span style="color:#a6e22e">wrapper!</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Feed</span>(ObjectSubclass<span style="color:#f92672">&lt;</span>imp::Feed<span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> Feed {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">new</span>(name: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>, uri: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; <span style="color:#a6e22e">Self</span> {
</span></span><span style="display:flex;"><span>        glib::Object::builder()
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;name&#34;</span>, name)
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;uri&#34;</span>, uri)
</span></span><span style="display:flex;"><span>            .build()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Three things stand out against that Part 2 sketch. I left the signals out: <code>items-updated</code> was the right instinct — feeds change when fresh content arrives — but with no fetching code yet, emitting it would be ceremony for nothing, so it comes back in Part 6 alongside the Tokio fetcher that gives it something to announce. The properties use <code>#[derive(Properties)]</code> paired with <code>#[glib::derived_properties]</code>, which is the modern declaration form: the macro generates the <code>properties()</code>, <code>set_property()</code>, and <code>property()</code> boilerplate from the struct fields, where the older tutorials you&rsquo;ll still find hand-roll <code>glib::ParamSpecString::builder(&quot;name&quot;).build()</code>. Identical runtime behaviour, dramatically less typing, one fewer place to fat-finger a property name. And each field sits in a <code>RefCell</code> because properties have to be settable through GObject&rsquo;s runtime API — that demands interior mutability, and <code>RefCell</code> is the simplest answer for fields written occasionally and read everywhere.</p>
<p>One thing about that <code>NAME</code> constant: it&rsquo;s the GObject type name registered with the runtime. Prefix it with the application — <code>GazetteFeed</code>, not just <code>Feed</code> — so it doesn&rsquo;t collide with another library&rsquo;s <code>Feed</code> type. Names are global; collisions abort the process at type registration. Once your app loads a <code>Feed</code> type, no other library in the same process can.</p>
<p>Wire the module into <code>src/main.rs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> application;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> config;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> feed;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> window;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> self::application::GazetteApplication;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> self::feed::Feed;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> self::window::GazetteWindow;
</span></span></code></pre></div><p><code>Feed</code> is now a real type the rest of the codebase can build on.</p>
<hr>
<h2 id="a-list-bound-to-a-model">A list bound to a model</h2>
<p>The sidebar in <code>window.blp</code> currently has one hard-coded row:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>ListBox feed_list {
</span></span><span style="display:flex;"><span>  selection-mode: single;
</span></span><span style="display:flex;"><span>  styles [&#34;navigation-sidebar&#34;]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  ListBoxRow {
</span></span><span style="display:flex;"><span>    child: Label {
</span></span><span style="display:flex;"><span>      label: _(&#34;All Articles&#34;);
</span></span><span style="display:flex;"><span>      halign: start;
</span></span><span style="display:flex;"><span>      margin-start: 12;
</span></span><span style="display:flex;"><span>      margin-end: 12;
</span></span><span style="display:flex;"><span>      margin-top: 6;
</span></span><span style="display:flex;"><span>      margin-bottom: 6;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That row is frozen in the markup. It can&rsquo;t grow, shrink, or change without touching the <code>.blp</code> file. Real apps don&rsquo;t work that way — feeds get added, removed, renamed. The markup needs to describe the <em>shape</em> of a row, and the runtime needs to produce one row per item in some collection. GTK has a name for this pattern: model/view binding.</p>
<p>Strip the inner <code>ListBoxRow</code> out. The new sidebar block:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>content: ScrolledWindow {
</span></span><span style="display:flex;"><span>  child: ListBox feed_list {
</span></span><span style="display:flex;"><span>    selection-mode: single;
</span></span><span style="display:flex;"><span>    styles [&#34;navigation-sidebar&#34;]
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Empty <code>ListBox</code>. The rows come from Rust now.</p>
<p>In <code>src/window.rs</code>, add the imports we&rsquo;ll need and grow the <code>imp</code> struct:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::cell::{OnceCell, RefCell};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> adw::subclass::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::{gio, glib};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#66d9ef">crate</span>::Feed;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">SAMPLE_FEEDS</span>: <span style="color:#66d9ef">&amp;</span>[(<span style="color:#f92672">&amp;</span><span style="color:#66d9ef">str</span>, <span style="color:#f92672">&amp;</span><span style="color:#66d9ef">str</span>)] <span style="color:#f92672">=</span> <span style="color:#f92672">&amp;</span>[
</span></span><span style="display:flex;"><span>    (<span style="color:#e6db74">&#34;This Week in GNOME&#34;</span>, <span style="color:#e6db74">&#34;https://thisweek.gnome.org/index.xml&#34;</span>),
</span></span><span style="display:flex;"><span>    (<span style="color:#e6db74">&#34;LWN.net Headlines&#34;</span>,  <span style="color:#e6db74">&#34;https://lwn.net/headlines/newrss&#34;</span>),
</span></span><span style="display:flex;"><span>    (<span style="color:#e6db74">&#34;Hacker News&#34;</span>,        <span style="color:#e6db74">&#34;https://hnrss.org/frontpage&#34;</span>),
</span></span><span style="display:flex;"><span>    (<span style="color:#e6db74">&#34;From the Architect&#34;</span>, <span style="color:#e6db74">&#34;https://fromthearchitect.dev/index.xml&#34;</span>),
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> imp {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">super</span>::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[derive(Debug, Default, gtk::CompositeTemplate)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template(resource = </span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette/window.ui&#34;</span><span style="color:#75715e">)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteWindow</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> split_view: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>adw::NavigationSplitView<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> feed_list: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>gtk::ListBox<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> placeholder: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>adw::StatusPage<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> feeds: <span style="color:#a6e22e">OnceCell</span><span style="color:#f92672">&lt;</span>gio::ListStore<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> selected_feed: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>Option<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;&gt;</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... ObjectSubclass impl unchanged
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>Two new fields. In the window&rsquo;s <code>ObjectImpl::constructed</code>, populate the store and bind it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> GazetteWindow {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">constructed</span>(<span style="color:#f92672">&amp;</span>self) {
</span></span><span style="display:flex;"><span>        self.parent_constructed();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> store <span style="color:#f92672">=</span> gio::ListStore::new::<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;</span>();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (name, uri) <span style="color:#66d9ef">in</span> <span style="color:#66d9ef">SAMPLE_FEEDS</span> {
</span></span><span style="display:flex;"><span>            store.append(<span style="color:#f92672">&amp;</span>Feed::new(name, uri));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        self.feeds.set(store.clone()).expect(<span style="color:#e6db74">&#34;feeds set once&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        self.feed_list.bind_model(Some(<span style="color:#f92672">&amp;</span>store), <span style="color:#f92672">|</span>item<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> feed <span style="color:#f92672">=</span> item.downcast_ref::<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;</span>().expect(<span style="color:#e6db74">&#34;item is a Feed&#34;</span>);
</span></span><span style="display:flex;"><span>            gtk::Label::builder()
</span></span><span style="display:flex;"><span>                .label(feed.name())
</span></span><span style="display:flex;"><span>                .halign(gtk::Align::Start)
</span></span><span style="display:flex;"><span>                .margin_start(<span style="color:#ae81ff">12</span>)
</span></span><span style="display:flex;"><span>                .margin_end(<span style="color:#ae81ff">12</span>)
</span></span><span style="display:flex;"><span>                .margin_top(<span style="color:#ae81ff">6</span>)
</span></span><span style="display:flex;"><span>                .margin_bottom(<span style="color:#ae81ff">6</span>)
</span></span><span style="display:flex;"><span>                .build()
</span></span><span style="display:flex;"><span>                .upcast()
</span></span><span style="display:flex;"><span>        });
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>bind_model</code> takes the store and a closure, and it accepts our store because <code>gio::ListStore</code> implements <code>gio::ListModel</code> — it binds against that trait, not the concrete type. That indirection is the part I actually care about: it&rsquo;s what lets me wrap the store in a sort or filter model <em>later</em> without rewriting this call, which is exactly the refactor Gazette grows into once feeds need ordering. The mechanics from there are unremarkable — <code>gtk::ListBox</code> runs the closure once per item at bind time, then again whenever the model&rsquo;s <code>items-changed</code> signal reports an add or change, and because the closure returns a bare <code>gtk::Widget</code>, <code>ListBox</code> wraps it in a <code>GtkListBoxRow</code> so we never construct one ourselves.</p>
<p>Run the app. The sidebar now shows four feeds — <em>This Week in GNOME</em>, <em>LWN.net Headlines</em>, <em>Hacker News</em>, <em>From the Architect</em> — and you can click between them. Selection still doesn&rsquo;t <em>do</em> anything, but the rows are real.</p>
<p>One trap worth flagging — and a clarification about what that generic actually buys you. <code>gio::ListStore::new::&lt;Feed&gt;()</code> parameterises the store on the Rust side, and that&rsquo;s worth doing: it keeps the typed API honest and documents what the store holds. But it&rsquo;s a Rust-side convenience, not a hard runtime guarantee — at the GObject level the store holds plain <code>glib::Object</code>. So items coming back <em>out</em> of it — through <code>item()</code>, or through the <code>ListModel</code> interface a sort or filter model would hand you — still arrive as <code>glib::Object</code> and need the explicit <code>and_downcast::&lt;Feed&gt;()</code> you&rsquo;ll see in the selection handler shortly. Parameterise with your concrete type, but treat it as intent and documentation, not as enforcement at the model boundary.</p>
<hr>
<h2 id="where-state-lives">Where state lives</h2>
<p>The <code>imp</code> struct now carries two things that aren&rsquo;t template children: <code>feeds</code> and <code>selected_feed</code>. This is the right place for them. <strong>State on a GObject lives in the inner <code>imp</code> struct, never on the outer wrapper.</strong></p>
<p>The reason is structural. <code>GazetteWindow</code> — the outer type — is a thin handle. <code>glib::wrapper!</code> makes it a transparent newtype around a refcounted pointer. Add a field to the outer type and you&rsquo;ve added a field that won&rsquo;t be shared between clones of the handle, won&rsquo;t be visible across closures, and won&rsquo;t survive being passed back through GTK&rsquo;s C API. None of those failure modes is loud; the code compiles, the field exists, you set it, and then a second handle to the &ldquo;same&rdquo; window can&rsquo;t see what you wrote.</p>
<p>The inner <code>imp::GazetteWindow</code>, by contrast, is the actual heap-allocated object. There&rsquo;s exactly one of it per window, all handles point to it, and any code holding a handle can reach into it via <code>obj.imp()</code>. State you want every part of the app to agree on goes there.</p>
<p>This is one of the framework&rsquo;s rules rather than something you can derive from first principles, and I learned it the slow way — by adding a field to the outer type, setting it, and then losing an afternoon to state that silently wasn&rsquo;t shared between two handles to the &ldquo;same&rdquo; window. Memorise it — <em>state lives in <code>imp</code></em> — and a surprising number of the architectural questions you might have about a GTK Rust app stop being questions.</p>
<h3 id="which-cell">Which cell?</h3>
<p><code>feeds: OnceCell&lt;gio::ListStore&gt;</code> and <code>selected_feed: RefCell&lt;Option&lt;Feed&gt;&gt;</code> — two different cells for two different lifetimes, and the difference is the whole point. The <code>feeds</code> store is built once in <code>constructed()</code> and read forever after, never replaced; that&rsquo;s exactly what <code>OnceCell</code> is for, and its <code>set</code> returning <code>Err</code> when it&rsquo;s already been set means the code asserts that contract for me instead of trusting me to remember it. The <code>selected_feed</code> changes every time the user picks a different row, so it needs the ongoing mutability <code>RefCell</code> gives. (A third category needs no cell at all: values the GObject machinery sets once at construction — template children, derived properties — that your own code never touches again.)</p>
<p><code>RefCell</code> adds a runtime borrow check: <code>borrow()</code> and <code>borrow_mut()</code> panic if you take incompatible borrows at once. In this domain that&rsquo;s exactly what I want — GTK callbacks run on the main thread one at a time, so the panic only fires when <em>my</em> code has gone re-entrant in a way I didn&rsquo;t notice, and the first time it happened to me it pointed straight at a bug class I&rsquo;d otherwise have spent an afternoon bisecting.</p>
<p>Notice the <code>gio::ListStore</code> itself isn&rsquo;t wrapped in <code>RefCell</code>, even though we mutate it constantly with <code>store.append(...)</code>. <code>ListStore</code> is a GObject, and GObjects do their own reference counting and stay mutable through their GObject API without needing Rust-side cells. Wrapping it would compile but be redundant — you&rsquo;d be borrow-checking access to a pointer.</p>
<p>My rule of thumb, after enough of these: template children and properties are naked fields with the right macro attribute; anything that owns a GObject — a <code>gio::ListStore</code>, another widget — is a naked field or an <code>OnceCell</code>, never a <code>RefCell</code>; a Rust value that genuinely mutates gets a <code>RefCell</code>, or a <code>Cell</code> if it&rsquo;s <code>Copy</code>; and a value set once and read everywhere gets a <code>OnceCell</code>. Once that split is reflexive, most of the &ldquo;which wrapper goes here?&rdquo; questions stop being questions.</p>
<hr>
<h2 id="wiring-selection-and-the-clone-macro">Wiring selection, and the <code>clone!</code> macro</h2>
<p>The rows render but selecting them does nothing. Wire selection up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> obj <span style="color:#f92672">=</span> self.obj();
</span></span><span style="display:flex;"><span>self.feed_list.connect_row_selected(glib::<span style="color:#a6e22e">clone!</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[weak]</span> obj,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_, row<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> feed <span style="color:#f92672">=</span> row
</span></span><span style="display:flex;"><span>            .and_then(<span style="color:#f92672">|</span>row<span style="color:#f92672">|</span> row.index().try_into().ok())
</span></span><span style="display:flex;"><span>            .and_then(<span style="color:#f92672">|</span>i: <span style="color:#66d9ef">u32</span><span style="color:#f92672">|</span> obj.imp().feeds.get().unwrap().item(i))
</span></span><span style="display:flex;"><span>            .and_downcast::<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        obj.imp().selected_feed.replace(feed.clone());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">let</span> Some(feed) <span style="color:#f92672">=</span> feed {
</span></span><span style="display:flex;"><span>            obj.emit_by_name::<span style="color:#f92672">&lt;</span>()<span style="color:#f92672">&gt;</span>(<span style="color:#e6db74">&#34;feed-selected&#34;</span>, <span style="color:#f92672">&amp;</span>[<span style="color:#f92672">&amp;</span>feed]);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>));
</span></span></code></pre></div><p>(We define the <code>feed-selected</code> signal in the next section. Pretend it exists for a moment.)</p>
<p>The <code>glib::clone!</code> macro is doing a lot of work. Without it, the equivalent is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> obj_weak <span style="color:#f92672">=</span> obj.downgrade();
</span></span><span style="display:flex;"><span>self.feed_list.connect_row_selected(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_, row<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> Some(obj) <span style="color:#f92672">=</span> obj_weak.upgrade() <span style="color:#66d9ef">else</span> { <span style="color:#66d9ef">return</span> };
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ... rest of the closure ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>});
</span></span></code></pre></div><p>The macro hides the <code>downgrade()</code> / <code>upgrade()</code> dance. The <code>#[weak]</code> attribute means the closure holds a <em>weak</em> reference to <code>obj</code>: it doesn&rsquo;t keep the window alive. When the closure runs, the macro tries to upgrade the weak reference; if the window is gone, the closure returns early.</p>
<p>The alternative is <code>#[strong]</code>, which captures by clone, incrementing the refcount. You almost never want that for a closure connected to one of the window&rsquo;s own widgets, because it builds a reference cycle:</p>
<ul>
<li>The window owns the widget.</li>
<li>The widget owns the signal handler (the closure).</li>
<li>The closure now strongly owns the window.</li>
</ul>
<p>The window can&rsquo;t drop because the widget&rsquo;s closure points to it; the widget can&rsquo;t drop because the window owns it; the closure can&rsquo;t drop because the widget holds it. Your app appears to leak windows on close. <code>#[weak]</code> breaks the cycle — the closure points weakly back at the window, and when the window&rsquo;s last strong reference is released, the chain unwinds cleanly.</p>
<p>This is the most common GTK Rust memory leak I run into, and it hides well: most apps have one window for their whole lifetime, so nothing looks wrong until you write a multi-window app — or, in my case, a test that spun windows up and tore them down in a loop, where the slowly climbing memory finally gave it away. The fix is always the same: weak the captures into widget-connected closures.</p>
<p>(One bit of translation if you&rsquo;re reading older code: the <code>clone!(@weak obj =&gt; move |...| {...})</code> syntax was deprecated in <code>glib</code> 0.20. The <code>#[weak]</code> attribute form is the current spelling. They behave identically.)</p>
<hr>
<h2 id="a-custom-signal-on-gazettewindow">A custom signal on <code>GazetteWindow</code></h2>
<p>The selection handler emits <code>feed-selected</code>, but we haven&rsquo;t defined it. Adding a custom signal is the same pattern as Part 2&rsquo;s <code>Feed::items-updated</code>, plugged into the window instead of the model.</p>
<p>In <code>mod imp</code> in <code>src/window.rs</code>, give <code>ObjectImpl</code> a <code>signals()</code> method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::sync::OnceLock;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> glib::subclass::Signal;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> GazetteWindow {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">signals</span>() -&gt; <span style="color:#66d9ef">&amp;</span>&#39;static [Signal] {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">SIGNALS</span>: <span style="color:#a6e22e">OnceLock</span><span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>Signal<span style="color:#f92672">&gt;&gt;</span> <span style="color:#f92672">=</span> OnceLock::new();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">SIGNALS</span>.get_or_init(<span style="color:#f92672">||</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">vec!</span>[Signal::builder(<span style="color:#e6db74">&#34;feed-selected&#34;</span>)
</span></span><span style="display:flex;"><span>                .param_types([Feed::static_type()])
</span></span><span style="display:flex;"><span>                .build()]
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">constructed</span>(<span style="color:#f92672">&amp;</span>self) {
</span></span><span style="display:flex;"><span>        self.parent_constructed();
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ... store setup, bind_model, connect_row_selected ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>signals()</code> is a static method GObject calls once, when the type is first registered, to learn what signals it defines. The <code>OnceLock</code> cache is conventional — GObject doesn&rsquo;t require it, but it means the <code>vec!</code> allocation only runs once. (Note this is <code>std::sync::OnceLock</code>, the thread-safe one: a <code>static</code> has to be <code>Sync</code>, so the single-threaded <code>std::cell::OnceCell</code> we reached for in the imp fields wouldn&rsquo;t compile here — same family, different guarantees.) Our signal is named <code>&quot;feed-selected&quot;</code> and carries one parameter, a <code>Feed</code>.</p>
<p>That&rsquo;s enough to make the <code>emit_by_name</code> call in the selection handler legal. But a signal with no listeners does nothing — so let&rsquo;s give it a consumer. The handler decides what to do when a feed is selected: update the placeholder.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> obj <span style="color:#f92672">=</span> self.obj();
</span></span><span style="display:flex;"><span>obj.connect_closure(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;feed-selected&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    glib::<span style="color:#a6e22e">closure_local!</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[weak]</span> obj,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_window: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">super</span>::GazetteWindow, feed: <span style="color:#a6e22e">Feed</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> imp <span style="color:#f92672">=</span> obj.imp();
</span></span><span style="display:flex;"><span>            imp.placeholder.set_icon_name(Some(<span style="color:#e6db74">&#34;rss-symbolic&#34;</span>));
</span></span><span style="display:flex;"><span>            imp.placeholder.set_title(<span style="color:#f92672">&amp;</span>feed.name());
</span></span><span style="display:flex;"><span>            imp.placeholder.set_description(Some(<span style="color:#f92672">&amp;</span>feed.uri()));
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    ),
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>A few new pieces:</p>
<ul>
<li><strong><code>connect_closure</code></strong> is the lower-level form of signal connection. We need it because the signal is custom — there&rsquo;s no auto-generated <code>connect_feed_selected</code> method, so we connect by name.</li>
<li><strong><code>glib::closure_local!</code></strong> wraps the Rust closure in a <code>glib::Closure</code> the GObject system can store. It&rsquo;s the bridge between Rust closures and GObject&rsquo;s signal machinery, and it accepts the same <code>#[weak]</code> / <code>#[strong]</code> attributes as <code>clone!</code>.</li>
<li><strong><code>false</code></strong> is the <code>after</code> flag — whether this handler runs before or after the <em>other</em> handlers connected to the signal. It&rsquo;s easy to read it as &ldquo;after the default handler,&rdquo; but that&rsquo;s a separate mechanism (the handler baked into the type definition); <code>after</code> only orders you against the other external connections. With a single handler it makes no difference; by convention, pass <code>false</code> unless you specifically need to run last.</li>
</ul>
<p>Two handlers now exist on <code>feed-selected</code>: the implicit one in <code>connect_row_selected</code> that <em>emits</em> it, and the explicit one in <code>connect_closure</code> that <em>consumes</em> it. Neither side names the other; the signal is all they share.</p>
<p>Run the app. Click <em>This Week in GNOME</em>; the content pane updates to show the feed name as the title and the URL as the description. Click <em>Hacker News</em>; it switches. Selection has meaning now.</p>
<hr>
<h2 id="when-rcrefcellt-isnt-the-answer">When <code>Rc&lt;RefCell&lt;T&gt;&gt;</code> isn&rsquo;t the answer</h2>
<p>The instinct, especially coming from non-GUI Rust, is to reach for <code>Rc&lt;RefCell&lt;T&gt;&gt;</code> whenever two closures need to share mutable state — it&rsquo;s the canonical answer to &ldquo;shared mutable state without locks,&rdquo; and my first couple of GTK apps were quietly littered with it.</p>
<p>Suppose both <code>connect_row_selected</code> and the <code>feed-selected</code> handler wanted to read the currently-selected <code>Feed</code>. The wrong instinct produces this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">// Don&#39;t do this.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">let</span> selected <span style="color:#f92672">=</span> std::rc::Rc::new(std::cell::RefCell::new(None::<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;</span>));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> selected_clone <span style="color:#f92672">=</span> selected.clone();
</span></span><span style="display:flex;"><span>self.feed_list.connect_row_selected(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_, row<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">*</span>selected_clone.borrow_mut() <span style="color:#f92672">=</span> <span style="color:#75715e">/* derive feed from row */</span> None;
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> selected_clone <span style="color:#f92672">=</span> selected.clone();
</span></span><span style="display:flex;"><span>obj.connect_closure(<span style="color:#e6db74">&#34;feed-selected&#34;</span>, <span style="color:#66d9ef">false</span>, glib::<span style="color:#a6e22e">closure_local!</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_window: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">super</span>::GazetteWindow, _feed: <span style="color:#a6e22e">Feed</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> _current <span style="color:#f92672">=</span> selected_clone.borrow();
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>));
</span></span></code></pre></div><p>This compiles, runs, and is reasonable Rust outside the GTK context. Inside it, the state belongs in <code>imp</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>self.feed_list.connect_row_selected(glib::<span style="color:#a6e22e">clone!</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[weak]</span> obj,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_, row<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> feed <span style="color:#f92672">=</span> <span style="color:#75715e">/* derive feed from row */</span>;
</span></span><span style="display:flex;"><span>        obj.imp().selected_feed.replace(feed);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>));
</span></span></code></pre></div><p>Both closures already reach the same <code>selected_feed</code> through <code>obj.imp()</code>. There&rsquo;s no need for an additional <code>Rc</code> — GObject&rsquo;s reference counting <em>is</em> the <code>Rc</code>. There&rsquo;s no need for a separate <code>RefCell</code> — the field is already in one on the imp struct.</p>
<p>The rule of thumb: <strong>any time you find yourself typing <code>Rc&lt;RefCell&lt;T&gt;&gt;</code> inside an <code>imp</code> block, stop.</strong> The state you&rsquo;re trying to share lives on the imp; closures reach it through <code>obj.imp()</code>; the GObject lifecycle handles the sharing. It can be appropriate at the <em>edges</em> of a system — prototyping a small example without a wrapping GObject — but inside a real GObject-based architecture, it&rsquo;s a smell.</p>
<hr>
<h2 id="binding-the-view-to-state">Binding the view to state</h2>
<p>The placeholder updates imperatively inside the <code>feed-selected</code> handler — three <code>set_*</code> calls per selection. <code>bind_property</code> promises to replace that with a declarative link, and the tempting move is to drop it straight into the handler:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">// Tempting, and wrong.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>feed.bind_property(<span style="color:#e6db74">&#34;name&#34;</span>, <span style="color:#f92672">&amp;*</span>imp.placeholder, <span style="color:#e6db74">&#34;title&#34;</span>)
</span></span><span style="display:flex;"><span>    .sync_create()
</span></span><span style="display:flex;"><span>    .build();
</span></span></code></pre></div><p>This compiles and looks right — select a feed, the title updates — and I shipped exactly this shape once and didn&rsquo;t notice for weeks. The catch is that the handler runs <em>once per selection</em>, so every click builds a fresh <code>glib::Binding</code> and nothing tears down the previous one. Click between four feeds and the placeholder&rsquo;s <code>title</code> now has four live bindings pointing at it, each holding a reference to a feed. They stay quiet only because a feed&rsquo;s <code>name</code> never changes after creation — and the moment it does, which is exactly what Part 6&rsquo;s fetcher will make happen, every stale binding fires and an <em>unselected</em> feed can overwrite the <em>selected</em> one&rsquo;s title. A binding built in a per-event handler is a leak with a delay on it.</p>
<p>The fix isn&rsquo;t to manage the binding&rsquo;s lifetime by hand. It&rsquo;s to notice what the placeholder actually is: a pure function of <em>which feed is selected</em>. That&rsquo;s state, and we have a rule for state — it lives in <code>imp</code>. Promote <code>selected_feed</code> from a plain field to a GObject property, and the placeholder can bind to it <strong>once</strong>, for the life of the window.</p>
<p>Add the <code>Properties</code> derive to the window&rsquo;s imp struct and annotate the field:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[properties(wrapper_type = super::GazetteWindow)]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[template(resource = </span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette/window.ui&#34;</span><span style="color:#75715e">)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteWindow</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> split_view: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>adw::NavigationSplitView<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> feed_list: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>gtk::ListBox<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> placeholder: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>adw::StatusPage<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> feeds: <span style="color:#a6e22e">OnceCell</span><span style="color:#f92672">&lt;</span>gio::ListStore<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[property(get, set, nullable)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> selected_feed: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>Option<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;&gt;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Add <code>#[glib::derived_properties]</code> above the window&rsquo;s <code>impl ObjectImpl</code> block — the same pairing <code>Feed</code> already uses. <code>nullable</code> is what lets the property hold <code>None</code>: nothing is selected at startup, and the user can clear the selection later.</p>
<p>The selection handler now <em>sets the property</em> instead of replacing the cell directly. Going through the generated setter is what emits <code>notify::selected-feed</code>, and that notification drives the binding:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>self.feed_list.connect_row_selected(glib::<span style="color:#a6e22e">clone!</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[weak]</span> obj,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_, row<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> feed <span style="color:#f92672">=</span> row
</span></span><span style="display:flex;"><span>            .and_then(<span style="color:#f92672">|</span>row<span style="color:#f92672">|</span> row.index().try_into().ok())
</span></span><span style="display:flex;"><span>            .and_then(<span style="color:#f92672">|</span>i: <span style="color:#66d9ef">u32</span><span style="color:#f92672">|</span> obj.imp().feeds.get().unwrap().item(i))
</span></span><span style="display:flex;"><span>            .and_downcast::<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        obj.set_selected_feed(feed.as_ref());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">let</span> Some(feed) <span style="color:#f92672">=</span> feed {
</span></span><span style="display:flex;"><span>            obj.emit_by_name::<span style="color:#f92672">&lt;</span>()<span style="color:#f92672">&gt;</span>(<span style="color:#e6db74">&#34;feed-selected&#34;</span>, <span style="color:#f92672">&amp;</span>[<span style="color:#f92672">&amp;</span>feed]);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>));
</span></span></code></pre></div><p>Then bind the placeholder to the property once, in <code>constructed</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>obj.bind_property(<span style="color:#e6db74">&#34;selected-feed&#34;</span>, <span style="color:#f92672">&amp;*</span>self.placeholder, <span style="color:#e6db74">&#34;title&#34;</span>)
</span></span><span style="display:flex;"><span>    .transform_to(<span style="color:#f92672">|</span>_, feed: Option<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;|</span> {
</span></span><span style="display:flex;"><span>        Some(feed.map_or_else(
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">||</span> <span style="color:#e6db74">&#34;No Feed Selected&#34;</span>.to_string(),
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">|</span>feed<span style="color:#f92672">|</span> feed.name(),
</span></span><span style="display:flex;"><span>        ))
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .sync_create()
</span></span><span style="display:flex;"><span>    .build();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>obj.bind_property(<span style="color:#e6db74">&#34;selected-feed&#34;</span>, <span style="color:#f92672">&amp;*</span>self.placeholder, <span style="color:#e6db74">&#34;description&#34;</span>)
</span></span><span style="display:flex;"><span>    .transform_to(<span style="color:#f92672">|</span>_, feed: Option<span style="color:#f92672">&lt;</span>Feed<span style="color:#f92672">&gt;|</span> {
</span></span><span style="display:flex;"><span>        Some(feed.map_or_else(
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">||</span> <span style="color:#e6db74">&#34;Select a feed from the sidebar to see its articles.&#34;</span>.to_string(),
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">|</span>feed<span style="color:#f92672">|</span> feed.uri(),
</span></span><span style="display:flex;"><span>        ))
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .sync_create()
</span></span><span style="display:flex;"><span>    .build();
</span></span></code></pre></div><p><code>transform_to</code> runs whenever the source changes, mapping the selected <code>Feed</code> to a string for the target. The <code>None</code> arm matters as much as the <code>Some</code> arm: when the selection clears, the binding restores the empty-state text — the placeholder handles deselection for free, with no code in the handler. <code>sync_create()</code> copies the current value (<code>None</code> at startup) the moment the binding is built, which is why the placeholder reads &ldquo;No Feed Selected&rdquo; before anything is clicked.</p>
<p>With the placeholder now driven by the property, the <code>feed-selected</code> <em>placeholder</em> handler has nothing left to do — delete it. And that deletion sharpens the point about routing through a signal. Reflecting state in the UI — the title just following the selection — is a binding&rsquo;s job: declarative, set up once. <em>Acting</em> on a change — fetching the feed, logging, anything with a side effect — is a signal&rsquo;s job, and <code>feed-selected</code> stays exactly for that. The placeholder was never really a signal consumer; it was a view of state, and now it&rsquo;s wired that way.</p>
<p>So what <em>does</em> listen to <code>feed-selected</code> now? For the moment, a logger — proof the seam works:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span>obj.connect_closure(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;feed-selected&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    glib::<span style="color:#a6e22e">closure_local!</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>_window: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">super</span>::GazetteWindow, feed: <span style="color:#a6e22e">Feed</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">eprintln!</span>(<span style="color:#e6db74">&#34;feed selected: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74"> (</span><span style="color:#e6db74">{}</span><span style="color:#e6db74">)&#34;</span>, feed.name(), feed.uri());
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    ),
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Run, click feeds, watch the terminal. Two independent things now happen on every selection: the placeholder updates through the property binding, and the log prints through the signal handler — and neither is wired to the other. Part 6 connects a second handler to the same signal, an async fetcher that reads the feed&rsquo;s URL, and it slots in without the binding or this logger ever knowing it arrived.</p>
<hr>
<h2 id="sharp-edges">Sharp edges</h2>
<p>A few things that bite, all on the GObject-runtime seam where Rust&rsquo;s compile-time guarantees stop and GObject&rsquo;s runtime conventions begin.</p>
<h3 id="signal-names-arent-checked">Signal names aren&rsquo;t checked</h3>
<p><code>emit_by_name::&lt;()&gt;(&quot;feed-selected&quot;, ...)</code> compiles fine if you typo the name. It fails at runtime when the type system can&rsquo;t find a signal by that exact string, and panics with a message about a name lookup miss. There&rsquo;s no compile-time check. The remedies: care, integration tests that exercise every signal path, or <code>const</code>-ing the name in one place and using it in both <code>signals()</code> and the emit sites.</p>
<h3 id="connect_row_selected-fires-with-none-on-deselection"><code>connect_row_selected</code> fires with <code>None</code> on deselection</h3>
<p>When the selection clears — programmatically, or because something else takes focus — <code>row</code> is <code>None</code>. Code that always assumes a feed exists goes quietly wrong. Branch on <code>row</code> early; the <code>Option</code> is part of the API for a reason.</p>
<h3 id="borrow-panics-inside-signal-handlers">Borrow panics inside signal handlers</h3>
<p>If you hold a <code>RefCell</code> borrow across a signal emission and a handler for that signal tries to borrow the same cell, you panic — signal handlers run synchronously, so the emit doesn&rsquo;t return until they&rsquo;re done. The shape that triggers it: <code>let mut state = self.thing.borrow_mut();</code> followed immediately by <code>obj.emit_by_name::&lt;()&gt;(...)</code>, still inside that borrow. The fix is to scope the borrow tightly so it ends before you emit. (The derived property setters are safe here: each scopes its own <code>borrow_mut</code>, writes, and returns <em>before</em> GObject emits <code>notify</code>.)</p>
<h3 id="kebab-case-at-the-gobject-level-snake_case-in-rust">Kebab-case at the GObject level, snake_case in Rust</h3>
<p>A struct field <code>selected_feed</code> becomes the property <code>selected-feed</code> in <code>bind_property</code> and friends, with accessors <code>selected_feed()</code> / <code>set_selected_feed(...)</code> on the Rust side. The derive macro does the conversion. Mistype the kebab-case form — <code>bind_property(&quot;selected_feed&quot;, ...)</code> — and the binding fails to find a property by that name. Not silently: GLib prints a <code>g_warning</code> to stderr and the binding does nothing.</p>
<hr>
<h2 id="what-we-have-so-far">What we have so far</h2>
<p>Selecting a feed does something. The structure model → list → selection → signal → content is in place, and four real RSS feeds populate the sidebar at startup. The state we added — <code>feeds: OnceCell&lt;gio::ListStore&gt;</code>, <code>selected_feed</code> as a nullable property — lives in <code>imp</code>, where the GObject machinery already manages sharing across closures and lifetimes. <code>Rc&lt;RefCell&lt;T&gt;&gt;</code> doesn&rsquo;t appear anywhere in this codebase, and probably won&rsquo;t.</p>
<p>The <code>feed-selected</code> signal is a deliberate seam. The placeholder doesn&rsquo;t even use it — selection state flows through a property binding instead, because reflecting state and reacting to events are different jobs. The signal is for reacting: right now a single logging handler listens.</p>
<hr>
<h2 id="what-comes-next">What comes next</h2>
<p>That logging handler is a placeholder for the real one. Part 6 — <strong>Fetching Feeds</strong> — connects an asynchronous fetcher to <code>feed-selected</code>: it hits each feed&rsquo;s URL, parses the RSS or Atom response, and emits the <code>items-updated</code> signal we deferred back at the top of this post as articles arrive.</p>
<p>That fetcher needs a runtime GTK doesn&rsquo;t have. Part 6 explains why GTK is single-threaded around a single main loop, why blocking that loop freezes the window, and how the two-executor pattern lets Tokio do network work without ever touching a widget on the wrong thread. The seam we built here is what it plugs into.</p>
<hr>
<p>The source code at the end of this post lives on the <code>part-5</code> branch of <a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-5" target="_blank" rel="noopener">fromthearchitect/gnome-rust-gazette</a>.</p>
]]></content:encoded></item><item><title>Building GNOME Apps with Rust, Part 4: Blueprint</title><link>https://fromthearchitect.dev/posts/gnome-rust-part-4-blueprint/?utm_source=rss</link><pubDate>Wed, 06 May 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-part-4-blueprint/</guid><description>Replace GTK&amp;#39;s verbose XML templates with Blueprint, wire it into Meson, and grow the Hello-World window into the real Gazette layout — sidebar, content pane, and a placeholder feed list.</description><content:encoded><![CDATA[<p><em>This is Part 4 of a series taking a GNOME app from an empty directory to GNOME Circle. <a href="/posts/gnome-rust-part-3-your-first-app/">Part 3</a> walked through every file Builder generated for our <code>gazette</code> project. Now we&rsquo;re going to start changing things.</em></p>
<p><em>If you&rsquo;re new to this stack and wondering why GTK and libadwaita are separate libraries, why GObject&rsquo;s type system feels like 1990s C, or why Flatpak ships its own runtime alongside your app, there&rsquo;s a <a href="/posts/gnome-rust-bonus-stack-underneath/">short companion piece on the history of the stack</a>. Skim it for context or skip it for code.</em></p>
<hr>
<h2 id="the-xml-problem">The XML problem</h2>
<p>At the end of Part 3, we had a working app. A header bar, a hamburger menu, a &ldquo;Hello, World!&rdquo; label, and a <code>window.ui</code> file that looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;interface&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;requires</span> <span style="color:#a6e22e">lib=</span><span style="color:#e6db74">&#34;gtk&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;4.0&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;requires</span> <span style="color:#a6e22e">lib=</span><span style="color:#e6db74">&#34;Adw&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1.0&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;template</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GazetteWindow&#34;</span> <span style="color:#a6e22e">parent=</span><span style="color:#e6db74">&#34;AdwApplicationWindow&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;title&#34;</span> <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>Gazette<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;default-width&#34;</span><span style="color:#f92672">&gt;</span>800<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;default-height&#34;</span><span style="color:#f92672">&gt;</span>600<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;content&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;AdwToolbarView&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;child</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;top&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;AdwHeaderBar&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;child</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;end&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GtkMenuButton&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;primary&#34;</span><span style="color:#f92672">&gt;</span>True<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;icon-name&#34;</span><span style="color:#f92672">&gt;</span>open-menu-symbolic<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;tooltip-text&#34;</span>
</span></span><span style="display:flex;"><span>                          <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>Main Menu<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;menu-model&#34;</span><span style="color:#f92672">&gt;</span>primary_menu<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;/child&gt;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;/child&gt;</span>
</span></span><span style="display:flex;"><span>        ...
</span></span></code></pre></div><p>Read that aloud. Notice how much of it is structural noise — <code>&lt;object class=&quot;...&quot;&gt;</code>, <code>&lt;property name=&quot;...&quot;&gt;</code>, opening and closing tags wrapping single values. The actual <em>information</em> — &ldquo;there&rsquo;s a header bar with a menu button on the right&rdquo; — is buried under a layer of XML scaffolding. And we haven&rsquo;t even built any UI yet.</p>
<p>This isn&rsquo;t a fixable problem in XML. It&rsquo;s how XML works. So GNOME has a different answer.</p>
<hr>
<h2 id="blueprint">Blueprint</h2>
<p><a href="https://gitlab.gnome.org/GNOME/blueprint-compiler" target="_blank" rel="noopener">Blueprint</a> is a markup language built for GTK. It compiles to the same <code>.ui</code> XML GTK has always loaded — the runtime artefact is identical — but the file you actually write looks like real code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-gdscript3" data-lang="gdscript3"><span style="display:flex;"><span>using Gtk <span style="color:#ae81ff">4.0</span>;
</span></span><span style="display:flex;"><span>using Adw <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>template <span style="color:#f92672">$</span>GazetteWindow : Adw<span style="color:#f92672">.</span>ApplicationWindow {
</span></span><span style="display:flex;"><span>  title: _(<span style="color:#e6db74">&#34;Gazette&#34;</span>);
</span></span><span style="display:flex;"><span>  default<span style="color:#f92672">-</span>width: <span style="color:#ae81ff">800</span>;
</span></span><span style="display:flex;"><span>  default<span style="color:#f92672">-</span>height: <span style="color:#ae81ff">600</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  content: Adw<span style="color:#f92672">.</span>ToolbarView {
</span></span><span style="display:flex;"><span>    [top]
</span></span><span style="display:flex;"><span>    Adw<span style="color:#f92672">.</span>HeaderBar {
</span></span><span style="display:flex;"><span>      [end]
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">MenuButton</span> {
</span></span><span style="display:flex;"><span>        primary: true;
</span></span><span style="display:flex;"><span>        icon<span style="color:#f92672">-</span>name: <span style="color:#e6db74">&#34;open-menu-symbolic&#34;</span>;
</span></span><span style="display:flex;"><span>        tooltip<span style="color:#f92672">-</span>text: _(<span style="color:#e6db74">&#34;Main Menu&#34;</span>);
</span></span><span style="display:flex;"><span>        menu<span style="color:#f92672">-</span>model: primary_menu;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    content: <span style="color:#a6e22e">Label</span> label {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;Hello, World!&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      styles [<span style="color:#e6db74">&#34;title-1&#34;</span>]
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>menu primary_menu {
</span></span><span style="display:flex;"><span>  section {
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_Preferences&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.preferences&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_Keyboard Shortcuts&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.shortcuts&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_About Gazette&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.about&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Same widget tree. Same template binding. Same translatable strings. About a third the line count, and you can actually scan the structure without your eyes glazing over.</p>
<p>Two bits of syntax are worth knowing up front. The <code>$</code> on <code>$GazetteWindow</code> is there because it&rsquo;s a user-defined type — the compiler can&rsquo;t validate it against a <code>.gir</code>, so the <code>$</code> is your acknowledgement that you know what you&rsquo;re doing. Built-in types like <code>Adw.ApplicationWindow</code> never need it. And <code>[top]</code>, <code>[end]</code> are child types, equivalent to <code>&lt;child type=&quot;top&quot;&gt;</code> in XML, placed immediately before the child they apply to. The rest — <code>using</code> for namespace imports, <code>_(&quot;...&quot;)</code> for translatable strings, <code>styles [&quot;...&quot;]</code> for CSS classes — you can pick up by porting.</p>
<hr>
<h2 id="wiring-blueprint-into-the-build">Wiring Blueprint into the build</h2>
<p>Blueprint is a separate compiler, not a built-in GTK feature. Before we change any UI, we need to teach Meson how to invoke it and update the GResource manifest to bundle the generated <code>.ui</code> files instead of the original ones.</p>
<h3 id="is-blueprint-compiler-available">Is <code>blueprint-compiler</code> available?</h3>
<p><code>blueprint-compiler</code> ships in the GNOME 50 SDK (which we upgraded to at the end of Part 3), so inside the Flatpak build sandbox we don&rsquo;t need to install anything. If you&rsquo;re targeting an older runtime where it isn&rsquo;t included, you&rsquo;d add it as a module in the Flatpak manifest:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span><span style="color:#e6db74">&#34;modules&#34;</span> <span style="color:#960050;background-color:#1e0010">:</span> [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span> : <span style="color:#e6db74">&#34;blueprint-compiler&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;buildsystem&#34;</span> : <span style="color:#e6db74">&#34;meson&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;cleanup&#34;</span> : [<span style="color:#e6db74">&#34;*&#34;</span>],
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;sources&#34;</span> : [
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;type&#34;</span> : <span style="color:#e6db74">&#34;git&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;url&#34;</span> : <span style="color:#e6db74">&#34;https://gitlab.gnome.org/GNOME/blueprint-compiler.git&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&#34;tag&#34;</span> : <span style="color:#e6db74">&#34;v0.20.4&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;name&#34;</span> : <span style="color:#e6db74">&#34;gazette&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#960050;background-color:#1e0010">...</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>For us on GNOME 50, that block isn&rsquo;t necessary. Just confirming the binary exists is enough.</p>
<h3 id="updating-srcmesonbuild">Updating <code>src/meson.build</code></h3>
<p>Open <code>src/meson.build</code>. We need to add a <code>custom_target</code> that runs <code>blueprint-compiler batch-compile</code> over our <code>.blp</code> files and produces matching <code>.ui</code> files in the build directory. Then we point the existing <code>gnome.compile_resources</code> call at those generated files via a dependency.</p>
<p>Here&rsquo;s the relevant addition, sitting just above the existing <code>compile_resources</code> call:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-meson" data-lang="meson"><span style="display:flex;"><span>blueprints <span style="color:#f92672">=</span> custom_target(<span style="color:#e6db74">&#39;blueprints&#39;</span>,
</span></span><span style="display:flex;"><span>  input: files(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;window.blp&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;shortcuts-dialog.blp&#39;</span>,
</span></span><span style="display:flex;"><span>  ),
</span></span><span style="display:flex;"><span>  output: <span style="color:#e6db74">&#39;.&#39;</span>,
</span></span><span style="display:flex;"><span>  command: [
</span></span><span style="display:flex;"><span>    find_program(<span style="color:#e6db74">&#39;blueprint-compiler&#39;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;batch-compile&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;@OUTPUT@&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;@CURRENT_SOURCE_DIR@&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;@INPUT@&#39;</span>,
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>gnome.compile_resources(<span style="color:#e6db74">&#39;gazette&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;gazette.gresource.xml&#39;</span>,
</span></span><span style="display:flex;"><span>  dependencies: blueprints,
</span></span><span style="display:flex;"><span>  gresource_bundle: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  install: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  install_dir: pkgdatadir,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The <code>dependencies: blueprints</code> line is what ties the two together. Without it, Meson might try to compile resources before Blueprint has produced the <code>.ui</code> files, and you&rsquo;ll get a mystifying &ldquo;file not found&rdquo; error mid-build.</p>
<h3 id="gazettegresourcexml-is-unchanged"><code>gazette.gresource.xml</code> is unchanged</h3>
<p>The resource manifest still references <code>window.ui</code> and <code>shortcuts-dialog.ui</code>. It doesn&rsquo;t mention <code>.blp</code> at all — and that&rsquo;s the part that catches people.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;gresources&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;gresource</span> <span style="color:#a6e22e">prefix=</span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;file</span> <span style="color:#a6e22e">preprocess=</span><span style="color:#e6db74">&#34;xml-stripblanks&#34;</span><span style="color:#f92672">&gt;</span>window.ui<span style="color:#f92672">&lt;/file&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;file</span> <span style="color:#a6e22e">preprocess=</span><span style="color:#e6db74">&#34;xml-stripblanks&#34;</span><span style="color:#f92672">&gt;</span>shortcuts-dialog.ui<span style="color:#f92672">&lt;/file&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/gresource&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/gresources&gt;</span>
</span></span></code></pre></div><p>That&rsquo;s because <code>gnome.compile_resources</code> looks for the listed files first in the source directory, then in the build directory — and our <code>custom_target</code> writes the generated <code>.ui</code> files into the build directory at exactly the right relative path. The Rust code referencing <code>/io/github/fromthearchitect/gazette/window.ui</code> doesn&rsquo;t know or care that the file went through a Blueprint compile step on the way in.</p>
<p>This is the bit I want you to internalise: <strong>Blueprint is purely a build-time concern</strong>. The runtime artefact is identical. If anything goes wrong at runtime, it&rsquo;s a GTK problem, not a Blueprint problem — and the error messages will reference XML structures because that&rsquo;s what GTK sees.</p>
<h3 id="updating-potfilesin">Updating <code>POTFILES.in</code></h3>
<p>Open <code>po/POTFILES.in</code>. Builder&rsquo;s scaffold lists one source file alongside the desktop integration files:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>data/io.github.fromthearchitect.gazette.desktop.in
</span></span><span style="display:flex;"><span>data/io.github.fromthearchitect.gazette.metainfo.xml.in
</span></span><span style="display:flex;"><span>data/io.github.fromthearchitect.gazette.gschema.xml
</span></span><span style="display:flex;"><span>src/window.ui
</span></span></code></pre></div><p>Two changes. First, point at <code>window.blp</code> instead of <code>window.ui</code>. Second, add <code>shortcuts-dialog.blp</code> — Builder doesn&rsquo;t list it, but the strings in there need extracting too:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>data/io.github.fromthearchitect.gazette.desktop.in
</span></span><span style="display:flex;"><span>data/io.github.fromthearchitect.gazette.metainfo.xml.in
</span></span><span style="display:flex;"><span>data/io.github.fromthearchitect.gazette.gschema.xml
</span></span><span style="display:flex;"><span>src/shortcuts-dialog.blp
</span></span><span style="display:flex;"><span>src/window.blp
</span></span></code></pre></div><p>Why point at the <code>.blp</code> files rather than the generated <code>.ui</code> files? Gettext extracts strings by parsing source files for known patterns — <code>_(&quot;...&quot;)</code> in Blueprint, <code>translatable=&quot;yes&quot;</code> in XML, <code>gettext!()</code> in Rust, etc. If <code>POTFILES.in</code> lists the <em>generated</em> <code>.ui</code> files, extraction will work, but the line numbers in your <code>.po</code> files will reference auto-generated paths in the build directory that change between builds. Pointing at the <code>.blp</code> source means translators see meaningful filenames and stable line numbers.</p>
<p>The Meson <code>i18n</code> module&rsquo;s <code>xgettext</code> invocation already scans for <code>_(...)</code> and <code>C_(...)</code> calls regardless of file type, so listing <code>.blp</code> files works without any Blueprint-specific extractor.</p>
<hr>
<h2 id="porting-the-existing-ui-files">Porting the existing UI files</h2>
<p>Now the actual conversion. Two files: <code>window.ui</code> and <code>shortcuts-dialog.ui</code>.</p>
<p>If you had dozens of files to convert, you&rsquo;d run <code>blueprint-compiler port</code> from the project root and it would scan the project, generate <code>.blp</code> versions of every <code>.ui</code> file, and update the references for you. We&rsquo;ve got two files, so it&rsquo;s just as quick to do it by hand and learn the syntax in the process.</p>
<h3 id="windowblp"><code>window.blp</code></h3>
<p>Delete <code>src/window.ui</code>. Create <code>src/window.blp</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-gdscript3" data-lang="gdscript3"><span style="display:flex;"><span>using Gtk <span style="color:#ae81ff">4.0</span>;
</span></span><span style="display:flex;"><span>using Adw <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>template <span style="color:#f92672">$</span>GazetteWindow : Adw<span style="color:#f92672">.</span>ApplicationWindow {
</span></span><span style="display:flex;"><span>  title: _(<span style="color:#e6db74">&#34;Gazette&#34;</span>);
</span></span><span style="display:flex;"><span>  default<span style="color:#f92672">-</span>width: <span style="color:#ae81ff">800</span>;
</span></span><span style="display:flex;"><span>  default<span style="color:#f92672">-</span>height: <span style="color:#ae81ff">600</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  content: Adw<span style="color:#f92672">.</span>ToolbarView {
</span></span><span style="display:flex;"><span>    [top]
</span></span><span style="display:flex;"><span>    Adw<span style="color:#f92672">.</span>HeaderBar {
</span></span><span style="display:flex;"><span>      [end]
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">MenuButton</span> {
</span></span><span style="display:flex;"><span>        primary: true;
</span></span><span style="display:flex;"><span>        icon<span style="color:#f92672">-</span>name: <span style="color:#e6db74">&#34;open-menu-symbolic&#34;</span>;
</span></span><span style="display:flex;"><span>        tooltip<span style="color:#f92672">-</span>text: _(<span style="color:#e6db74">&#34;Main Menu&#34;</span>);
</span></span><span style="display:flex;"><span>        menu<span style="color:#f92672">-</span>model: primary_menu;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    content: <span style="color:#a6e22e">Label</span> label {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;Hello, World!&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      styles [<span style="color:#e6db74">&#34;title-1&#34;</span>]
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>menu primary_menu {
</span></span><span style="display:flex;"><span>  section {
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_Preferences&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.preferences&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_Keyboard Shortcuts&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.shortcuts&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_About Gazette&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.about&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Compare against the original XML if you want — every property, child, and string carries across one-to-one.</p>
<p>The one thing worth pointing out is <code>Label label</code>. In Blueprint, when you want to give a widget an ID (so the Rust side can grab it as a <code>TemplateChild</code>), you put the ID after the type, with no <code>id:</code> keyword. It feels weird at first if you&rsquo;re coming from XML&rsquo;s <code>&lt;object class=&quot;GtkLabel&quot; id=&quot;label&quot;&gt;</code>, but it&rsquo;s consistent with how the rest of the language treats names.</p>
<h3 id="shortcuts-dialogblp"><code>shortcuts-dialog.blp</code></h3>
<p>Builder&rsquo;s template for <code>shortcuts-dialog.ui</code> is built around <code>Gtk.ShortcutsWindow</code>, which was deprecated in GTK 4.18; libadwaita 1.8 shipped a successor, <code>Adw.ShortcutsDialog</code>. Since Part 3 put us on GNOME 50 (libadwaita 1.9), we&rsquo;ll port to the modern widget while we&rsquo;re already in the file.</p>
<p>Delete <code>src/shortcuts-dialog.ui</code> and create <code>src/shortcuts-dialog.blp</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>using Gtk 4.0;
</span></span><span style="display:flex;"><span>using Adw 1;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Adw.ShortcutsDialog shortcuts_dialog {
</span></span><span style="display:flex;"><span>  Adw.ShortcutsSection {
</span></span><span style="display:flex;"><span>    title: C_(&#34;shortcut window&#34;, &#34;General&#34;);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Adw.ShortcutsItem {
</span></span><span style="display:flex;"><span>      title: C_(&#34;shortcut window&#34;, &#34;Show Shortcuts&#34;);
</span></span><span style="display:flex;"><span>      action-name: &#34;app.shortcuts&#34;;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Adw.ShortcutsItem {
</span></span><span style="display:flex;"><span>      title: C_(&#34;shortcut window&#34;, &#34;Quit&#34;);
</span></span><span style="display:flex;"><span>      action-name: &#34;app.quit&#34;;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>C_(&quot;context&quot;, &quot;string&quot;)</code> is the Blueprint equivalent of XML&rsquo;s <code>&lt;attribute name=&quot;label&quot; translatable=&quot;yes&quot; context=&quot;shortcut window&quot;&gt;</code> — gettext context lets translators distinguish the same English word used in different UI contexts. Builder generates these for shortcut dialogs because &ldquo;General&rdquo; might translate differently depending on whether it&rsquo;s a settings category or a shortcut group.</p>
<p>The other thing worth knowing about this file is the widget ID. <code>shortcuts_dialog</code> is what <code>AdwApplication</code> looks for: if a <code>shortcuts-dialog.ui</code> resource exists at the application&rsquo;s resource base path and its root widget is an <code>AdwShortcutsDialog</code> with that ID, the application class automatically installs an <code>app.shortcuts</code> action that presents the dialog, plus a Ctrl+? accelerator. That&rsquo;s why Part 3&rsquo;s menu could reference <code>app.shortcuts</code> even though we never defined it ourselves.</p>
<p>The migration mapping, if you&rsquo;re updating an older project: <code>Gtk.ShortcutsWindow</code> → <code>Adw.ShortcutsDialog</code>, <code>Gtk.ShortcutsGroup</code> → <code>Adw.ShortcutsSection</code>, <code>Gtk.ShortcutsShortcut</code> → <code>Adw.ShortcutsItem</code>. The old &ldquo;view → section → group → shortcut&rdquo; hierarchy collapsed to &ldquo;section → item&rdquo;. Two features didn&rsquo;t survive the trip: gesture shortcuts and per-shortcut icons. If you needed those, you stay on the deprecated widget. We don&rsquo;t, so we don&rsquo;t.</p>
<p><img alt="Gazette&rsquo;s keyboard shortcuts dialog rendered as an Adw.ShortcutsDialog, showing the General section with Show Shortcuts and Quit entries" loading="lazy" src="/images/gnome-rust-part-4/shortcuts-dialog.png"></p>
<h3 id="build-it">Build it</h3>
<p>Hit <strong>Run</strong>. If everything is wired correctly, the app builds and launches looking exactly like it did at the end of Part 3.</p>
<p>If you get build errors, the most common causes are:</p>
<ul>
<li><strong>Forgot the <code>dependencies: blueprints</code> line</strong> — error references <code>window.ui</code> not found.</li>
<li><strong>Typo in a Blueprint file</strong> — the compiler error is good, but it points at line numbers in the <code>.blp</code>, not the generated XML.</li>
<li><strong>Old <code>.ui</code> file still on disk</strong> — if you copied the <code>.blp</code> instead of replacing the <code>.ui</code>, both exist and the resource bundle will pick up whichever Meson finds first.</li>
</ul>
<p>A clean rebuild (Builder&rsquo;s <strong>Build → Clean Build Output</strong>) clears up most of these.</p>
<hr>
<h2 id="now-we-have-a-foundation-lets-actually-use-it">Now we have a foundation. Let&rsquo;s actually use it.</h2>
<p>A &ldquo;Hello, World!&rdquo; label is fine for proving the pipeline works. It is not Gazette. Gazette needs a sidebar of feeds and an articles pane. On a phone, only one shows at a time.</p>
<p>Libadwaita has a widget for this exact pattern: <code>Adw.NavigationSplitView</code>. Side-by-side on a wide screen, single-pane navigation when collapsed — at least, once you wire up the breakpoint that tells it when to collapse. We&rsquo;ll get to that.</p>
<p>Replace the contents of <code>window.blp</code> with this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-gdscript3" data-lang="gdscript3"><span style="display:flex;"><span>using Gtk <span style="color:#ae81ff">4.0</span>;
</span></span><span style="display:flex;"><span>using Adw <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>template <span style="color:#f92672">$</span>GazetteWindow : Adw<span style="color:#f92672">.</span>ApplicationWindow {
</span></span><span style="display:flex;"><span>  title: _(<span style="color:#e6db74">&#34;Gazette&#34;</span>);
</span></span><span style="display:flex;"><span>  default<span style="color:#f92672">-</span>width: <span style="color:#ae81ff">1000</span>;
</span></span><span style="display:flex;"><span>  default<span style="color:#f92672">-</span>height: <span style="color:#ae81ff">700</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  Adw<span style="color:#f92672">.</span>Breakpoint {
</span></span><span style="display:flex;"><span>    condition (<span style="color:#e6db74">&#34;max-width: 600sp&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    setters {
</span></span><span style="display:flex;"><span>      split_view<span style="color:#f92672">.</span>collapsed: true;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  content: Adw<span style="color:#f92672">.</span>NavigationSplitView split_view {
</span></span><span style="display:flex;"><span>    sidebar: Adw<span style="color:#f92672">.</span>NavigationPage {
</span></span><span style="display:flex;"><span>      title: _(<span style="color:#e6db74">&#34;Feeds&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      child: Adw<span style="color:#f92672">.</span>ToolbarView {
</span></span><span style="display:flex;"><span>        [top]
</span></span><span style="display:flex;"><span>        Adw<span style="color:#f92672">.</span>HeaderBar {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        content: ScrolledWindow {
</span></span><span style="display:flex;"><span>          child: ListBox feed_list {
</span></span><span style="display:flex;"><span>            selection<span style="color:#f92672">-</span>mode: single;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            styles [<span style="color:#e6db74">&#34;navigation-sidebar&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            ListBoxRow {
</span></span><span style="display:flex;"><span>              child: <span style="color:#a6e22e">Label</span> {
</span></span><span style="display:flex;"><span>                label: _(<span style="color:#e6db74">&#34;All Articles&#34;</span>);
</span></span><span style="display:flex;"><span>                halign: start;
</span></span><span style="display:flex;"><span>                margin<span style="color:#f92672">-</span>start: <span style="color:#ae81ff">12</span>;
</span></span><span style="display:flex;"><span>                margin<span style="color:#f92672">-</span>end: <span style="color:#ae81ff">12</span>;
</span></span><span style="display:flex;"><span>                margin<span style="color:#f92672">-</span>top: <span style="color:#ae81ff">6</span>;
</span></span><span style="display:flex;"><span>                margin<span style="color:#f92672">-</span>bottom: <span style="color:#ae81ff">6</span>;
</span></span><span style="display:flex;"><span>              };
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          };
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    content: Adw<span style="color:#f92672">.</span>NavigationPage {
</span></span><span style="display:flex;"><span>      title: _(<span style="color:#e6db74">&#34;Articles&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      child: Adw<span style="color:#f92672">.</span>ToolbarView {
</span></span><span style="display:flex;"><span>        [top]
</span></span><span style="display:flex;"><span>        Adw<span style="color:#f92672">.</span>HeaderBar {
</span></span><span style="display:flex;"><span>          [end]
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">MenuButton</span> {
</span></span><span style="display:flex;"><span>            primary: true;
</span></span><span style="display:flex;"><span>            icon<span style="color:#f92672">-</span>name: <span style="color:#e6db74">&#34;open-menu-symbolic&#34;</span>;
</span></span><span style="display:flex;"><span>            tooltip<span style="color:#f92672">-</span>text: _(<span style="color:#e6db74">&#34;Main Menu&#34;</span>);
</span></span><span style="display:flex;"><span>            menu<span style="color:#f92672">-</span>model: primary_menu;
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        content: Adw<span style="color:#f92672">.</span>StatusPage placeholder {
</span></span><span style="display:flex;"><span>          icon<span style="color:#f92672">-</span>name: <span style="color:#e6db74">&#34;rss-symbolic&#34;</span>;
</span></span><span style="display:flex;"><span>          title: _(<span style="color:#e6db74">&#34;No Feed Selected&#34;</span>);
</span></span><span style="display:flex;"><span>          description: _(<span style="color:#e6db74">&#34;Select a feed from the sidebar to see its articles.&#34;</span>);
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>menu primary_menu {
</span></span><span style="display:flex;"><span>  section {
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_Preferences&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.preferences&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_Keyboard Shortcuts&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.shortcuts&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    item {
</span></span><span style="display:flex;"><span>      label: _(<span style="color:#e6db74">&#34;_About Gazette&#34;</span>);
</span></span><span style="display:flex;"><span>      action: <span style="color:#e6db74">&#34;app.about&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The window is bigger by default — 1000x700 instead of 800x600 — to give two panes room to breathe. <code>Adw.NavigationSplitView</code> replaces the single <code>Adw.ToolbarView</code>, with a <code>sidebar</code> and a <code>content</code>, each holding an <code>Adw.NavigationPage</code>. Each pane gets its own <code>Adw.ToolbarView</code> and <code>Adw.HeaderBar</code> — the libadwaita pattern is that every pane owns its own chrome, so when the layout collapses on narrow screens each one has a working header bar with title and back button. The sidebar&rsquo;s <code>ListBox</code> carries the <code>navigation-sidebar</code> style class for the standard look; the content pane shows an <code>Adw.StatusPage</code> empty state using <code>rss-symbolic</code> from <code>adwaita-icon-theme</code>. The hamburger menu has moved to the content pane&rsquo;s header bar — that&rsquo;s where it lives in most GNOME apps with this layout (Files, Music, Lollypop, Apostrophe).</p>
<p>The bit that catches people: <code>Adw.NavigationSplitView</code> doesn&rsquo;t collapse on its own. Drop the window narrower without an <code>Adw.Breakpoint</code> and it just stays side-by-side, ignoring you. The breakpoint is what flips <code>split_view.collapsed</code> to <code>true</code> when the window narrows below 600 scaled pixels — adaptive behaviour is a property you set, not something the widget figures out. The <code>sp</code> unit (scaled pixels) means the threshold respects the user&rsquo;s text scaling factor, so the layout collapses at the same effective width regardless of how big they&rsquo;ve made their fonts.</p>
<p>Hit <strong>Run</strong> again.</p>
<p><img alt="Gazette running with the new layout: Feeds sidebar on the left with a single All Articles row, and an Articles pane on the right showing a No Feed Selected status page" loading="lazy" src="/images/gnome-rust-part-4/main-window-with-sidebar.png"></p>
<p>The hamburger menu wires up unchanged — same <code>app.shortcuts</code>, <code>app.about</code>, and (still unimplemented) <code>app.preferences</code> actions Builder generated in Part 3:</p>
<p><img alt="Gazette&rsquo;s primary menu open from the content pane&rsquo;s header bar, showing Preferences, Keyboard Shortcuts, and About Gazette entries" loading="lazy" src="/images/gnome-rust-part-4/menu-open.png"></p>
<p>And dragging the window narrower trips the breakpoint, collapsing the split view to single-pane navigation:</p>
<img src="/images/gnome-rust-part-4/collapsed-window.png" alt="Gazette in narrow width with the split view collapsed — only the Feeds sidebar is visible, with a single All Articles row" style="max-width: 360px; display: block; margin: 0 auto;" />
<hr>
<h2 id="the-rust-side">The Rust side</h2>
<p>Open <code>src/window.rs</code>. Right now it has one <code>TemplateChild</code> for the label we just deleted:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug, Default, gtk::CompositeTemplate)]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[template(resource = </span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette/window.ui&#34;</span><span style="color:#75715e">)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteWindow</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> label: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>gtk::Label<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two things to update. First, the <code>label</code> field doesn&rsquo;t refer to anything any more — there&rsquo;s no <code>Label label</code> in the new <code>window.blp</code>. Second, we want typed handles to the new widgets so future posts can react to them.</p>
<p>Replace the struct with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[derive(Debug, Default, gtk::CompositeTemplate)]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[template(resource = </span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette/window.ui&#34;</span><span style="color:#75715e">)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteWindow</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> split_view: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>adw::NavigationSplitView<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> feed_list: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>gtk::ListBox<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> placeholder: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>adw::StatusPage<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Note that the <code>#[template(resource = ...)]</code> path is unchanged. We didn&rsquo;t rename the resource path when we switched to Blueprint — <code>window.ui</code> is still what gets bundled into the gresource at <code>/io/github/fromthearchitect/gazette/window.ui</code>, it&rsquo;s just generated from <code>window.blp</code> now. Keeping the resource path stable is what lets the rest of the code be oblivious to the source format change.</p>
<p>Build and run. Same window, three new typed handles available for when we need them.</p>
<hr>
<h2 id="sharp-edges">Sharp edges</h2>
<h3 id="editor-support">Editor support</h3>
<p>The official Blueprint compiler ships an LSP server. In <strong>GNOME Builder</strong>, support is built in — you get syntax highlighting, completion, and inline error markers automatically. In <strong>VS Code</strong>, install the <a href="https://marketplace.visualstudio.com/items?itemName=bodil.blueprint-gtk" target="_blank" rel="noopener">Blueprint extension</a>. In <strong>Neovim</strong>, configure <code>nvim-lspconfig</code> with <code>blueprint_ls</code>. The LSP knows about every property and signal of every widget — if you mistype <code>defautl-width</code>, you&rsquo;ll see the squiggle before you save.</p>
<h3 id="error-message-lineage">Error message lineage</h3>
<p>When Blueprint fails to compile, the error references your <code>.blp</code> file with helpful line and column numbers. When GTK fails to load a <code>.ui</code> template at runtime — usually because you referenced a widget that doesn&rsquo;t exist, or the <code>parent</code> doesn&rsquo;t match the Rust <code>ParentType</code> — the error references the <em>generated</em> <code>.ui</code> file. The line numbers won&rsquo;t match your <code>.blp</code>. That&rsquo;s fine, the structure does, but it catches everyone the first time.</p>
<h3 id="blueprint-cant-express-everything">Blueprint can&rsquo;t express everything</h3>
<p>Blueprint covers the GTK Builder XML format completely, but a handful of edge cases require dropping back to either raw XML or runtime Rust code. The most common one I hit is custom GtkBuilder-loaded scriptable types where a property takes a complex serialised value. If you need it, you can mix <code>.ui</code> and <code>.blp</code> files in the same gresource — just list the <code>.ui</code> ones in the <code>gazette.gresource.xml</code> manifest and skip them in the Blueprint custom target.</p>
<h3 id="dont-commit-the-generated-ui-files">Don&rsquo;t commit the generated <code>.ui</code> files</h3>
<p>The build directory holds the generated XML. Builder&rsquo;s default <code>.gitignore</code> already excludes <code>_build/</code>, but if you set up a different build layout, double-check. Committing generated <code>.ui</code> files leads to merge conflicts every time someone tweaks a <code>.blp</code>.</p>
<hr>
<h2 id="what-we-have-so-far">What we have so far</h2>
<p>A <code>window.blp</code> and <code>shortcuts-dialog.blp</code> instead of XML. A working sidebar + content layout that&rsquo;s adaptive on small screens. Three typed widget handles in Rust waiting for behaviour. The same single-instance app, the same gresource path, the same menu, the same translations — but the file you actually edit when you want to change the UI now reads like a tree, not a tag soup.</p>
<hr>
<h2 id="what-comes-next">What comes next</h2>
<p>We have widgets. We don&rsquo;t have any logic. Clicking a row in the sidebar does nothing. Adding a feed isn&rsquo;t possible. There&rsquo;s no state.</p>
<p>Part 5 is <strong>App Architecture Patterns</strong> — how to organise mutable state in a Rust GTK app without losing your mind. The big shape of it: GTK widgets aren&rsquo;t <code>Send</code>, <code>Rc&lt;RefCell&lt;T&gt;&gt;</code> is everywhere, the <code>clone!</code> macro keeps closures sane, and async work lives on Tokio while UI work lives on the GLib main loop. We&rsquo;ll set up the two-executor pattern and use it to wire the sidebar selection to the content pane via a real GObject signal.</p>
<p>That&rsquo;s the post where the patterns from Part 2 stop being theoretical.</p>
<hr>
<p>The source code at the end of this post lives on the <code>part-4</code> branch of <a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-4" target="_blank" rel="noopener">fromthearchitect/gnome-rust-gazette</a>.</p>
]]></content:encoded></item><item><title>Building GNOME Apps with Rust, Bonus: The Stack Underneath</title><link>https://fromthearchitect.dev/posts/gnome-rust-bonus-stack-underneath/?utm_source=rss</link><pubDate>Tue, 05 May 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-bonus-stack-underneath/</guid><description>Why GNOME&amp;#39;s stack looks the way it does in 2026 — a short tour of the licence fight, the component dream that didn&amp;#39;t pan out, the GNOME 3 reset, the libadwaita split, and the Flatpak runtime model.</description><content:encoded><![CDATA[<p><em>This is a bonus post in the series taking a GNOME app from an empty directory to GNOME Circle. It sits between <a href="/posts/gnome-rust-part-3-your-first-app/">Part 3</a> and <a href="/posts/gnome-rust-part-4-blueprint/">Part 4</a> — read it if you want context, skip it if you want code.</em></p>
<hr>
<p>By the end of Part 3 you had a running GTK 4 + libadwaita app with five trait implementations, a <code>mod imp</code> block, an <code>adw::Application</code> parent class, a <code>meson.build</code> next to your <code>Cargo.toml</code>, and a Flatpak manifest pinning a runtime version. None of those decisions arrived in 2026 fully formed. Each is a fossil of a specific argument that took years to settle.</p>
<p>This is the short version of those arguments. The platform you&rsquo;re writing against is shaped by five fights — over licence, components, design, toolkit independence, and distribution — and once you&rsquo;ve heard the fights, the conventions stop feeling arbitrary.</p>
<hr>
<h2 id="1-the-licence-fight-19962000">1. The licence fight (1996–2000)</h2>
<p>In 1996 the dominant Unix GUI toolkit was <strong>Motif</strong>, owned by the Open Software Foundation and proprietary in a way that mattered: you couldn&rsquo;t ship free software against it without commercial licensing. Spencer Kimball and Peter Mattis, two Berkeley undergrads writing the <strong>GIMP</strong> paint program, wrote their own toolkit instead and called it the <em>GIMP Toolkit</em> — <strong>GTK</strong>. Within a few years the toolkit had outgrown the paint program. It is now what most of the free desktop is built on.</p>
<p>The same year, Trolltech released <strong>Qt 1.0</strong> and Matthias Ettrich announced <strong>KDE</strong> on top of it. Qt was technically excellent, but its licence wasn&rsquo;t one the Free Software Foundation recognised until 2000. Between 1996 and 2000, a substantial part of the GNU community considered KDE non-free regardless of source availability.</p>
<p>In August 1997, Miguel de Icaza and Federico Mena announced <strong>GNOME</strong> — explicitly framed as a free-software desktop that didn&rsquo;t depend on Qt. The technical justifications (component model, accessibility, language bindings) were in the announcement; the political one — <em>you should not have to choose between a working desktop and a free-software stack</em> — is what gave the project momentum. Qt was relicensed under the GPL in 2000 and the original objection mostly evaporated, but by then GNOME existed, had momentum, and had a 1.0 release on the way. The schism was permanent.</p>
<p>The through-line to your code: GNOME&rsquo;s founding pressure was <em>licence purity</em>, not technical superiority. Builder defaults to GPL-3.0-or-later when it scaffolds a project. Flathub&rsquo;s review process flags proprietary dependencies. Publishing through Flatpak rather than as a closed binary is implicit in every tutorial you&rsquo;ll read. None of this is convention; it&rsquo;s the residue of an argument from 1996.</p>
<hr>
<h2 id="2-the-component-dream-that-didnt-pan-out-19992008">2. The component dream that didn&rsquo;t pan out (1999–2008)</h2>
<p>GNOME 1.0 shipped in 1999 against GTK+ 1.2. The project&rsquo;s name was the <em>GNU Network Object Model Environment</em>, and the <em>network object model</em> part was load-bearing. GNOME wasn&rsquo;t supposed to be a window manager and a panel. It was supposed to be a full component architecture: applications built out of embeddable, scriptable, network-transparent objects living inside each other&rsquo;s windows. The technology underneath was <strong>CORBA</strong>, and GNOME shipped its own implementation (<strong>ORBit</strong>) plus a higher-level component framework (<strong>Bonobo</strong>). The dream was that you&rsquo;d embed a <strong>GNumeric</strong> spreadsheet inside an <strong>AbiWord</strong> document and edit it in place.</p>
<p>This wasn&rsquo;t a GNOME-specific bet. The whole industry was on it: Microsoft&rsquo;s COM and OLE, Sun&rsquo;s Java RMI, a decade of conference talks predicting that desktop apps would be rebuilt as orchestrations of small composable objects messaging each other. It didn&rsquo;t pan out. Components-via-IPC was the wrong abstraction at the application layer, and the lesson got learned the expensive way by everyone who&rsquo;d bet on it.</p>
<p>The technology survived in a different shape. <strong>GLib 2.0</strong> had shipped in 2002, and one of its contributions was to formalise <strong>GObject</strong> as a standalone type system — separable from GTK, and from the Bonobo component dream above it. That is the type system you&rsquo;re working against in Part 2&rsquo;s <code>Feed</code> and Part 3&rsquo;s <code>GazetteWindow</code>. The shape of it — inner/outer split, vtables, manual reference counting, registration macros — is a 1990s C solution to the object-oriented programming problem. It feels strange in Rust because it predates Rust by fifteen years. <strong>D-Bus</strong> survived for the same reason: deliberately small where CORBA was deliberately general, and it&rsquo;s how your single-instance behaviour works. <strong>GObject Introspection</strong> (2008) ships machine-readable type metadata alongside every GObject library, and it&rsquo;s the only reason the Rust bindings can be auto-generated and stay current.</p>
<p>A lot of GNOME&rsquo;s strangeness in 2026 is the residue of a 1990s bet that didn&rsquo;t pay off. The bet itself is gone. The infrastructure built to support it survived, got refactored, and is now the foundation everything else stands on.</p>
<hr>
<h2 id="3-the-gnome-3-reset-2011">3. The GNOME 3 reset (2011)</h2>
<p>GNOME 2 had been the standard Linux desktop for a decade by 2010 — a panel, a menu, a taskbar, icons on the desktop. Boring, in the best sense.</p>
<p>April 2011 broke that. <strong>GNOME 3.0</strong> shipped with a complete reset of the user-visible desktop: a full-screen Activities overview, dynamic workspaces, no taskbar, no minimise button, no system tray, no desktop icons. The argument from the design team was that those affordances were artefacts of a 1990s desktop that no longer matched how people actually used computers. The revolt was real and loud — whole distributions refused to ship it, two forks (<strong>MATE</strong> preserving GNOME 2 verbatim, <strong>Cinnamon</strong> fixing the new shell) emerged within a year, and Linus Torvalds publicly switched to Xfce.</p>
<p>Here&rsquo;s the part that&rsquo;s harder to write in 2026 than in 2012: the design has aged well. The Activities overview, dynamic workspaces, the deliberate absence of customisation knobs, the bias toward defending users from a thousand small decisions — these were unpopular in 2011 because they were unfamiliar. macOS, Windows 11, ChromeOS, every mobile OS — all the design idioms now look more like GNOME 3 than they look like GNOME 2.</p>
<p>Every libadwaita widget you reach for is a descendant of that design ethos. <code>Adw.ApplicationWindow</code> instead of <code>Gtk.Window</code>, <code>Adw.ToolbarView</code> containing the header bar, the absence of a system-tray API, the deliberately limited theming surface — these aren&rsquo;t arbitrary library decisions. They are the same opinionated-design argument from 2011, restated as code. Building Gazette to libadwaita conventions is, structurally, accepting GNOME 3&rsquo;s premise. Fifteen years on, the premise has stopped being controversial.</p>
<hr>
<h2 id="4-gtk-4-and-the-libadwaita-split-20202021">4. GTK 4 and the libadwaita split (2020–2021)</h2>
<p><strong>GTK 4.0</strong> shipped in December 2020 after roughly four years of development. The whole rendering pipeline had been rewritten — widgets emit declarative render nodes that GSK (the new GTK Scene Kit) compiles into GPU-accelerated draw operations. That&rsquo;s why your Flatpak manifest from Part 3 has <code>--device=dri</code> even though you&rsquo;re shipping an RSS reader. It&rsquo;s not for graphics; it&rsquo;s because GSK exists, and GSK wants the GPU.</p>
<p>The other GTK 4 change is harder to see but more consequential. The toolkit was deliberately positioned this time <em>not</em> to be the GNOME-only library any more. GTK 3 had dragged the GNOME aesthetic along with it wherever it went. GTK 4 strips the GNOME-specific visual identity out and leaves a place where it can be added back, optionally, as a separate library.</p>
<p>That library is <strong>libadwaita</strong>, first released in December 2021. Its release announcement put the bargain plainly: GTK should grow independently of GNOME at its own pace; other GTK consumers — <strong>Inkscape</strong>, <strong>elementary OS</strong> — should be first-class citizens rather than second-class baggage; libadwaita itself should be <em>another library you can choose to link against if you want to make your application fit well into GNOME</em>. The Adwaita stylesheet — GTK 3&rsquo;s default look — moved out of GTK into libadwaita, and GTK 4 kept a renamed neutral copy.</p>
<p>Each <code>adw::</code> import in <code>application.rs</code> and <code>window.rs</code> is a deliberate vote to be a GNOME app rather than a generic GTK one. Replace every <code>adw::</code> symbol with its <code>gtk::</code> equivalent and Gazette would still compile, still run, and still display a working window. It would just stop being a GNOME application. The line between the two is exactly the line GTK and libadwaita&rsquo;s architects drew in 2020 and 2021, restated in your <code>use</code> statements.</p>
<hr>
<h2 id="5-flatpak-and-the-runtime-model-2015">5. Flatpak and the runtime model (2015–)</h2>
<p>Before Flatpak, a GNOME application&rsquo;s distribution problem was N × M × K. Forty-odd Linux distributions, each shipping a different GTK version, each on a different cadence, often two GNOME releases behind. When a user reported a bug you&rsquo;d fixed eighteen months earlier, the answer was almost always <em>which distro, which version, please file a bug with them</em>. That isn&rsquo;t a release model. It&rsquo;s an apology.</p>
<p><strong>Flatpak</strong> (originally <strong>xdg-app</strong>, started 2015 by Alex Larsson at Red Hat) ignores the host distribution&rsquo;s library versions entirely. Bundle the application with the runtime it expects, run it in a sandbox via <strong>bubblewrap</strong>, mediate everything else through <strong>portals</strong>. If your app needs <code>org.gnome.Platform</code> 50, Flatpak ships it alongside your app and the user&rsquo;s Debian or Fedora version is no longer your problem.</p>
<p>The runtime/SDK distinction in Part 3 is the literal embodiment of this model. <code>org.gnome.Platform</code> is what users get; <code>org.gnome.Sdk</code> is what you build against. Updating your manifest from <code>&quot;runtime-version&quot;: &quot;48&quot;</code> to <code>&quot;50&quot;</code> isn&rsquo;t a build-system quirk. It&rsquo;s the act of saying <em>I have tested this app against GNOME 50, here is what I&rsquo;m shipping</em>. The Flatpak manifest pinning a version, the <code>--socket=wayland</code> line, the <code>--device=dri</code> line — every one of those is a public commitment to a specific platform contract.</p>
<hr>
<h2 id="so">So</h2>
<p>Each shape in your Part 3 project has a date and a reason now. The five trait implementations: GObject, formalised in 2002 because the 1990s component dream needed a separable type system. The <code>mod imp</code> block: that same 1990s C inheritance pattern restated as a Rust idiom. The <code>adw::Application</code> parent class: the 2020–2021 split between a deliberately neutral GTK 4 and a deliberately opinionated libadwaita, drawn in <code>use</code> statements. The <code>meson.build</code> next to <code>Cargo.toml</code>: the 2017–2019 migration that replaced autotools across the project. The Flatpak manifest: the 2015-onward distribution model that made <em>same app, all distributions</em> a viable promise.</p>
<p>History over. <a href="/posts/gnome-rust-part-4-blueprint/">Part 4</a> is where the code resumes.</p>
]]></content:encoded></item><item><title>Building GNOME Apps with Rust, Part 3: Your First App</title><link>https://fromthearchitect.dev/posts/gnome-rust-part-3-your-first-app/?utm_source=rss</link><pubDate>Sun, 03 May 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-part-3-your-first-app/</guid><description>Create a GTK4 + libadwaita application with GNOME Builder, understand every file it generates, and upgrade to the latest GNOME platform</description><content:encoded><![CDATA[<p><em>This is Part 3 of a series that takes a GNOME application from an empty directory to acceptance into GNOME Circle. <a href="/posts/gnome-rust-part-2-gobject/">Part 2</a> covered GObject&rsquo;s type system — properties, signals, and the inner/outer type pattern. Now we&rsquo;ll use everything we learned to build a real application.</em></p>
<hr>
<h2 id="from-theory-to-a-running-window">From theory to a running window</h2>
<p>In Part 2 we built a GObject subclass by hand — a <code>Feed</code> model with properties and signals, no GTK in sight. That was deliberate. Understanding GObject&rsquo;s inner/outer type split, the <code>ObjectSubclass</code> trait, and the <code>mod imp</code> pattern is the foundation that everything else rests on.</p>
<p>Now we&rsquo;re going to see those same patterns in context. By the end of this post, you&rsquo;ll have a running GTK4 + libadwaita application with a header bar, a menu, keyboard shortcuts, and a properly structured project that&rsquo;s ready for Flatpak packaging. And you won&rsquo;t have written most of it by hand — GNOME Builder will generate it for you. Our job is to understand what it generated and why.</p>
<hr>
<h2 id="creating-the-project-in-gnome-builder">Creating the project in GNOME Builder</h2>
<p>Open GNOME Builder and click <strong>Create New Project</strong>. Fill in the dialog like this:</p>
<p><img alt="GNOME Builder&rsquo;s Create New Project dialog, configured for Gazette" loading="lazy" src="/images/gnome-rust-part-3/start-a-new-project.png"></p>
<ul>
<li><strong>Project Name</strong>: <code>gazette</code></li>
<li><strong>Application ID</strong>: <code>io.github.fromthearchitect.gazette</code></li>
<li><strong>Location</strong>: wherever you keep your projects</li>
<li><strong>Language</strong>: Rust</li>
<li><strong>License</strong>: GPL-3.0-or-later</li>
<li><strong>Version Control</strong>: enabled</li>
<li><strong>Template</strong>: GNOME Application</li>
</ul>
<p>Click <strong>Create Project</strong>.</p>
<h3 id="a-note-on-application-ids">A note on application IDs</h3>
<p>The application ID is a reverse domain name that uniquely identifies your app across the entire Linux desktop. It&rsquo;s used for the D-Bus service name, the GSettings schema path, the desktop file name, the icon name, and the Flatpak bundle ID. Everything keys off this single string.</p>
<p>The convention is to use a domain you control. If you&rsquo;re publishing to a GitHub organisation, <code>io.github.yourorg.yourapp</code> is the standard pattern. If you own a domain, use it: <code>dev.fromthearchitect.gazette</code> would also work. The important thing is that this ID is globally unique and stable — changing it later means renaming dozens of files and updating every reference.</p>
<hr>
<h2 id="hit-run">Hit Run</h2>
<p>Before we look at any code, hit the <strong>Run</strong> button (the play icon in the header bar) or press <strong>Ctrl+F5</strong>. Builder will configure the Flatpak environment, download the SDK if needed, compile the Rust code, bundle the resources, and launch the application.</p>
<p>The first build takes a while — Cargo is downloading and compiling every dependency inside the Flatpak sandbox. Subsequent builds are much faster thanks to caching.</p>
<p>When it finishes, you&rsquo;ll see this:</p>
<p><img alt="The Gazette application running — an empty window with a header bar showing &ldquo;Gazette&rdquo; and a hamburger menu" loading="lazy" src="/images/gnome-rust-part-3/running-app.png"></p>
<p>A window. A header bar with a title. A hamburger menu with Preferences, Keyboard Shortcuts, and About entries. A &ldquo;Hello, World!&rdquo; label in the centre. That&rsquo;s your application.</p>
<p>It doesn&rsquo;t look like much yet, but there&rsquo;s a surprising amount happening behind the scenes. Let&rsquo;s look at what Builder actually created.</p>
<hr>
<h2 id="the-project-structure">The project structure</h2>
<p><img alt="GNOME Builder showing the Gazette project file tree" loading="lazy" src="/images/gnome-rust-part-3/project-after-creation.png"></p>
<p>Here&rsquo;s what Builder generated:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>gazette/
</span></span><span style="display:flex;"><span>├── Cargo.toml
</span></span><span style="display:flex;"><span>├── COPYING
</span></span><span style="display:flex;"><span>├── meson.build
</span></span><span style="display:flex;"><span>├── io.github.fromthearchitect.gazette.json
</span></span><span style="display:flex;"><span>├── data/
</span></span><span style="display:flex;"><span>│   ├── meson.build
</span></span><span style="display:flex;"><span>│   ├── icons/
</span></span><span style="display:flex;"><span>│   │   ├── meson.build
</span></span><span style="display:flex;"><span>│   │   ├── hicolor/scalable/apps/
</span></span><span style="display:flex;"><span>│   │   │   └── io.github.fromthearchitect.gazette.svg
</span></span><span style="display:flex;"><span>│   │   └── hicolor/symbolic/apps/
</span></span><span style="display:flex;"><span>│   │       └── io.github.fromthearchitect.gazette-symbolic.svg
</span></span><span style="display:flex;"><span>│   ├── io.github.fromthearchitect.gazette.desktop.in
</span></span><span style="display:flex;"><span>│   ├── io.github.fromthearchitect.gazette.gschema.xml
</span></span><span style="display:flex;"><span>│   ├── io.github.fromthearchitect.gazette.metainfo.xml.in
</span></span><span style="display:flex;"><span>│   └── io.github.fromthearchitect.gazette.service.in
</span></span><span style="display:flex;"><span>├── po/
</span></span><span style="display:flex;"><span>│   ├── LINGUAS
</span></span><span style="display:flex;"><span>│   ├── POTFILES.in
</span></span><span style="display:flex;"><span>│   └── meson.build
</span></span><span style="display:flex;"><span>└── src/
</span></span><span style="display:flex;"><span>    ├── meson.build
</span></span><span style="display:flex;"><span>    ├── main.rs
</span></span><span style="display:flex;"><span>    ├── application.rs
</span></span><span style="display:flex;"><span>    ├── config.rs.in
</span></span><span style="display:flex;"><span>    ├── window.rs
</span></span><span style="display:flex;"><span>    ├── window.ui
</span></span><span style="display:flex;"><span>    ├── shortcuts-dialog.ui
</span></span><span style="display:flex;"><span>    └── gazette.gresource.xml
</span></span></code></pre></div><p>That&rsquo;s a lot of files for &ldquo;Hello, World!&rdquo; — and every single one of them is there for a reason. Here&rsquo;s what&rsquo;s in each group.</p>
<hr>
<h2 id="the-rust-source-code">The Rust source code</h2>
<h3 id="mainrs--the-entry-point">main.rs — the entry point</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> application;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> config;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> window;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> self::application::GazetteApplication;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> self::window::GazetteWindow;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> config::{<span style="color:#66d9ef">GETTEXT_PACKAGE</span>, <span style="color:#66d9ef">LOCALEDIR</span>, <span style="color:#66d9ef">PKGDATADIR</span>};
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain};
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::{gio, glib};
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">main</span>() -&gt; <span style="color:#a6e22e">glib</span>::ExitCode {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Set up gettext translations
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    bindtextdomain(<span style="color:#66d9ef">GETTEXT_PACKAGE</span>, <span style="color:#66d9ef">LOCALEDIR</span>)
</span></span><span style="display:flex;"><span>        .expect(<span style="color:#e6db74">&#34;Unable to bind the text domain&#34;</span>);
</span></span><span style="display:flex;"><span>    bind_textdomain_codeset(<span style="color:#66d9ef">GETTEXT_PACKAGE</span>, <span style="color:#e6db74">&#34;UTF-8&#34;</span>)
</span></span><span style="display:flex;"><span>        .expect(<span style="color:#e6db74">&#34;Unable to set the text domain encoding&#34;</span>);
</span></span><span style="display:flex;"><span>    textdomain(<span style="color:#66d9ef">GETTEXT_PACKAGE</span>)
</span></span><span style="display:flex;"><span>        .expect(<span style="color:#e6db74">&#34;Unable to switch to the text domain&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Load resources
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">let</span> resources <span style="color:#f92672">=</span> gio::Resource::load(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">PKGDATADIR</span>.to_owned() <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;/gazette.gresource&#34;</span>
</span></span><span style="display:flex;"><span>    ).expect(<span style="color:#e6db74">&#34;Could not load resources&#34;</span>);
</span></span><span style="display:flex;"><span>    gio::resources_register(<span style="color:#f92672">&amp;</span>resources);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> app <span style="color:#f92672">=</span> GazetteApplication::new(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;io.github.fromthearchitect.gazette&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&amp;</span>gio::ApplicationFlags::empty(),
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    app.run()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Three things happen here before the application starts:</p>
<ol>
<li>
<p><strong>Gettext initialisation.</strong> This wires up the translation system so that strings marked as translatable in the UI files and source code can be looked up in the user&rsquo;s language. Even if you never translate your app, the plumbing needs to be in place.</p>
</li>
<li>
<p><strong>Resource loading.</strong> The compiled GResource bundle (containing the UI templates, icons, and other assets) is loaded from disk and registered globally. After this call, any code can access bundled files by their resource path — for example, <code>/io/github/fromthearchitect/gazette/window.ui</code>.</p>
</li>
<li>
<p><strong>Application creation and run.</strong> <code>GazetteApplication::new()</code> creates our application object, and <code>app.run()</code> hands control to the GLib main loop. The main loop processes events — user input, window management, D-Bus messages — until the application quits.</p>
</li>
</ol>
<p>The <code>config</code> module isn&rsquo;t a regular Rust file — it&rsquo;s generated at build time by Meson from a template. More on that shortly.</p>
<h3 id="applicationrs--the-gtkapplication-subclass">application.rs — the GtkApplication subclass</h3>
<p>This is where you&rsquo;ll recognise the GObject patterns from Part 2:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gettextrs::gettext;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> adw::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> adw::subclass::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::{gio, glib};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#66d9ef">crate</span>::config::<span style="color:#66d9ef">VERSION</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#66d9ef">crate</span>::GazetteWindow;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> imp {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">super</span>::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[derive(Debug, Default)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteApplication</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[glib::object_subclass]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectSubclass <span style="color:#66d9ef">for</span> GazetteApplication {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">NAME</span>: <span style="color:#66d9ef">&amp;</span>&#39;static <span style="color:#66d9ef">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;GazetteApplication&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Type</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">super</span>::GazetteApplication;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ParentType</span> <span style="color:#f92672">=</span> adw::Application;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> GazetteApplication {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">constructed</span>(<span style="color:#f92672">&amp;</span>self) {
</span></span><span style="display:flex;"><span>            self.parent_constructed();
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> obj <span style="color:#f92672">=</span> self.obj();
</span></span><span style="display:flex;"><span>            obj.setup_gactions();
</span></span><span style="display:flex;"><span>            obj.set_accels_for_action(<span style="color:#e6db74">&#34;app.quit&#34;</span>, <span style="color:#f92672">&amp;</span>[<span style="color:#e6db74">&#34;&lt;control&gt;q&#34;</span>]);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ApplicationImpl <span style="color:#66d9ef">for</span> GazetteApplication {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">activate</span>(<span style="color:#f92672">&amp;</span>self) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> application <span style="color:#f92672">=</span> self.obj();
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">let</span> window <span style="color:#f92672">=</span> application.active_window().unwrap_or_else(<span style="color:#f92672">||</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">let</span> window <span style="color:#f92672">=</span> GazetteWindow::new(<span style="color:#f92672">&amp;*</span>application);
</span></span><span style="display:flex;"><span>                window.upcast()
</span></span><span style="display:flex;"><span>            });
</span></span><span style="display:flex;"><span>            window.present();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> GtkApplicationImpl <span style="color:#66d9ef">for</span> GazetteApplication {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> AdwApplicationImpl <span style="color:#66d9ef">for</span> GazetteApplication {}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>glib::<span style="color:#a6e22e">wrapper!</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteApplication</span>(ObjectSubclass<span style="color:#f92672">&lt;</span>imp::GazetteApplication<span style="color:#f92672">&gt;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">@</span>extends gio::Application, gtk::Application, adw::Application,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">@</span>implements gio::ActionGroup, gio::ActionMap;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Same pattern as Part 2&rsquo;s <code>Feed</code>: an inner type in <code>mod imp</code> that holds the state, <code>ObjectSubclass</code> to register it with GObject, and a <code>glib::wrapper!</code> macro to create the outer type. The difference is what we&rsquo;re subclassing — <code>adw::Application</code> instead of <code>glib::Object</code> — and the trait implementations that come with it.</p>
<p><strong><code>ObjectImpl::constructed</code></strong> is called once when the application is first created. It&rsquo;s the place to set up actions and keyboard shortcuts. This is analogous to a constructor, but in GObject-land, construction happens in phases — <code>constructed</code> runs after all properties have been set.</p>
<p><strong><code>ApplicationImpl::activate</code></strong> is the most important callback. It&rsquo;s called when the application is launched (or when the user tries to open a second instance). The pattern here is standard: check if there&rsquo;s already a window, create one if there isn&rsquo;t, and present it. This is how GNOME apps implement single-instance behaviour — the platform enforces one running instance per application ID, and subsequent launches just activate the existing one.</p>
<p>The <code>impl GtkApplicationImpl</code> and <code>impl AdwApplicationImpl</code> lines are empty but required. They tell the GObject type system that we&rsquo;re implementing the full trait chain from <code>gio::Application</code> through <code>gtk::Application</code> to <code>adw::Application</code>. If you leave one out, you&rsquo;ll get a compile error.</p>
<p>Further down in the file, there&rsquo;s the actions setup and the About dialog:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> GazetteApplication {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">new</span>(application_id: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>, flags: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">gio</span>::ApplicationFlags) -&gt; <span style="color:#a6e22e">Self</span> {
</span></span><span style="display:flex;"><span>        glib::Object::builder()
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;application-id&#34;</span>, application_id)
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;flags&#34;</span>, flags)
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;resource-base-path&#34;</span>,
</span></span><span style="display:flex;"><span>                       <span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette&#34;</span>)
</span></span><span style="display:flex;"><span>            .build()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">setup_gactions</span>(<span style="color:#f92672">&amp;</span>self) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> quit_action <span style="color:#f92672">=</span> gio::ActionEntry::builder(<span style="color:#e6db74">&#34;quit&#34;</span>)
</span></span><span style="display:flex;"><span>            .activate(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>app: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">Self</span>, _, _<span style="color:#f92672">|</span> app.quit())
</span></span><span style="display:flex;"><span>            .build();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> about_action <span style="color:#f92672">=</span> gio::ActionEntry::builder(<span style="color:#e6db74">&#34;about&#34;</span>)
</span></span><span style="display:flex;"><span>            .activate(<span style="color:#66d9ef">move</span> <span style="color:#f92672">|</span>app: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">Self</span>, _, _<span style="color:#f92672">|</span> app.show_about())
</span></span><span style="display:flex;"><span>            .build();
</span></span><span style="display:flex;"><span>        self.add_action_entries([quit_action, about_action]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">show_about</span>(<span style="color:#f92672">&amp;</span>self) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> window <span style="color:#f92672">=</span> self.active_window().unwrap();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> about <span style="color:#f92672">=</span> adw::AboutDialog::builder()
</span></span><span style="display:flex;"><span>            .application_name(<span style="color:#e6db74">&#34;Gazette&#34;</span>)
</span></span><span style="display:flex;"><span>            .application_icon(<span style="color:#e6db74">&#34;io.github.fromthearchitect.gazette&#34;</span>)
</span></span><span style="display:flex;"><span>            .developer_name(<span style="color:#e6db74">&#34;Unknown&#34;</span>)
</span></span><span style="display:flex;"><span>            .version(<span style="color:#66d9ef">VERSION</span>)
</span></span><span style="display:flex;"><span>            .developers(<span style="color:#a6e22e">vec!</span>[<span style="color:#e6db74">&#34;Unknown&#34;</span>])
</span></span><span style="display:flex;"><span>            .translator_credits(<span style="color:#f92672">&amp;</span>gettext(<span style="color:#e6db74">&#34;translator-credits&#34;</span>))
</span></span><span style="display:flex;"><span>            .copyright(<span style="color:#e6db74">&#34;© 2026 Unknown&#34;</span>)
</span></span><span style="display:flex;"><span>            .build();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        about.present(Some(<span style="color:#f92672">&amp;</span>window));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notice the placeholder values — <code>developer_name(&quot;Unknown&quot;)</code>, <code>developers(vec![&quot;Unknown&quot;])</code>, <code>copyright(&quot;© 2026 Unknown&quot;)</code>. Builder generates these as placeholders. Replace them with your actual name before publishing your app.</p>
<p><strong>GActions</strong> are GNOME&rsquo;s action system. Instead of connecting button clicks directly to functions, you define named actions (<code>app.quit</code>, <code>app.about</code>) and connect UI elements to those names. The menu items in the UI template reference <code>app.about</code> — the action system routes that to <code>show_about()</code>. This decoupling means you can trigger the same action from a menu, a keyboard shortcut, a command-line argument, or a D-Bus message.</p>
<p>The <code>resource-base-path</code> property tells the application where to look for automatically loaded resources. When we set it to <code>/io/github/fromthearchitect/gazette</code>, the application will automatically load the shortcuts dialog from that path without us doing anything extra.</p>
<h3 id="windowrs--the-application-window">window.rs — the application window</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> adw::subclass::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::{gio, glib};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> imp {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> <span style="color:#66d9ef">super</span>::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[derive(Debug, Default, gtk::CompositeTemplate)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[template(resource = </span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette/window.ui&#34;</span><span style="color:#75715e">)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteWindow</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[template_child]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">pub</span> label: <span style="color:#a6e22e">TemplateChild</span><span style="color:#f92672">&lt;</span>gtk::Label<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[glib::object_subclass]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectSubclass <span style="color:#66d9ef">for</span> GazetteWindow {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">NAME</span>: <span style="color:#66d9ef">&amp;</span>&#39;static <span style="color:#66d9ef">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;GazetteWindow&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Type</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">super</span>::GazetteWindow;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ParentType</span> <span style="color:#f92672">=</span> adw::ApplicationWindow;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">class_init</span>(klass: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">mut</span> Self::Class) {
</span></span><span style="display:flex;"><span>            klass.bind_template();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">instance_init</span>(obj: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">glib</span>::subclass::InitializingObject<span style="color:#f92672">&lt;</span>Self<span style="color:#f92672">&gt;</span>) {
</span></span><span style="display:flex;"><span>            obj.init_template();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> GazetteWindow {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> WidgetImpl <span style="color:#66d9ef">for</span> GazetteWindow {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> WindowImpl <span style="color:#66d9ef">for</span> GazetteWindow {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ApplicationWindowImpl <span style="color:#66d9ef">for</span> GazetteWindow {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> AdwApplicationWindowImpl <span style="color:#66d9ef">for</span> GazetteWindow {}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>glib::<span style="color:#a6e22e">wrapper!</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">GazetteWindow</span>(ObjectSubclass<span style="color:#f92672">&lt;</span>imp::GazetteWindow<span style="color:#f92672">&gt;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">@</span>extends gtk::Widget, gtk::Window, gtk::ApplicationWindow,
</span></span><span style="display:flex;"><span>                 adw::ApplicationWindow,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">@</span>implements gio::ActionGroup, gio::ActionMap;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> GazetteWindow {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">new</span><span style="color:#f92672">&lt;</span>P: <span style="color:#a6e22e">IsA</span><span style="color:#f92672">&lt;</span>gtk::Application<span style="color:#f92672">&gt;&gt;</span>(application: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">P</span>) -&gt; <span style="color:#a6e22e">Self</span> {
</span></span><span style="display:flex;"><span>        glib::Object::builder()
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;application&#34;</span>, application)
</span></span><span style="display:flex;"><span>            .build()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><em>The <code>@implements</code> list above is truncated for readability — the actual generated code also includes <code>gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager</code>. See the source on the <a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-3" target="_blank" rel="noopener"><code>part-3</code> branch</a> for the full version.</em></p>
<p>This introduces two new concepts: <strong>composite templates</strong> and the <strong>widget trait chain</strong>.</p>
<p><strong><code>#[derive(gtk::CompositeTemplate)]</code></strong> connects this Rust struct to a UI definition file. The <code>#[template(resource = &quot;...&quot;)]</code> attribute tells GTK where to find the template, and <code>#[template_child]</code> fields create typed references to named widgets in that template. Here, <code>self.label</code> gives us direct access to the <code>GtkLabel</code> widget with <code>id=&quot;label&quot;</code> defined in the UI file.</p>
<p>The <code>class_init</code> and <code>instance_init</code> functions bind the template at the class level (once) and initialise it for each instance. This is boilerplate that every composite template widget needs.</p>
<p>The trait chain — <code>WidgetImpl</code>, <code>WindowImpl</code>, <code>ApplicationWindowImpl</code>, <code>AdwApplicationWindowImpl</code> — reflects the GTK class hierarchy. <code>AdwApplicationWindow</code> extends <code>gtk::ApplicationWindow</code>, which extends <code>gtk::Window</code>, which extends <code>gtk::Widget</code>. Each level of the hierarchy can override behaviour. For now, all these implementations are empty, but they must be present.</p>
<hr>
<h2 id="the-ui-template">The UI template</h2>
<h3 id="windowui">window.ui</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;interface&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;requires</span> <span style="color:#a6e22e">lib=</span><span style="color:#e6db74">&#34;gtk&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;4.0&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;requires</span> <span style="color:#a6e22e">lib=</span><span style="color:#e6db74">&#34;Adw&#34;</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;1.0&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;template</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GazetteWindow&#34;</span> <span style="color:#a6e22e">parent=</span><span style="color:#e6db74">&#34;AdwApplicationWindow&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;title&#34;</span> <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>Gazette<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;default-width&#34;</span><span style="color:#f92672">&gt;</span>800<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;default-height&#34;</span><span style="color:#f92672">&gt;</span>600<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;content&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;AdwToolbarView&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;child</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;top&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;AdwHeaderBar&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;child</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;end&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GtkMenuButton&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;primary&#34;</span><span style="color:#f92672">&gt;</span>True<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;icon-name&#34;</span><span style="color:#f92672">&gt;</span>open-menu-symbolic<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;tooltip-text&#34;</span>
</span></span><span style="display:flex;"><span>                          <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>Main Menu<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;menu-model&#34;</span><span style="color:#f92672">&gt;</span>primary_menu<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;/child&gt;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;/child&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;content&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GtkLabel&#34;</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;label&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;label&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>Hello, World!<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;style&gt;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">&lt;class</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;title-1&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;/style&gt;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/template&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;menu</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;primary_menu&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;section&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;item&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;label&#34;</span>
</span></span><span style="display:flex;"><span>                   <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>_Preferences<span style="color:#f92672">&lt;/attribute&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;action&#34;</span><span style="color:#f92672">&gt;</span>app.preferences<span style="color:#f92672">&lt;/attribute&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;/item&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;item&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;label&#34;</span>
</span></span><span style="display:flex;"><span>                   <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>_Keyboard Shortcuts<span style="color:#f92672">&lt;/attribute&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;action&#34;</span><span style="color:#f92672">&gt;</span>app.shortcuts<span style="color:#f92672">&lt;/attribute&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;/item&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;item&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;label&#34;</span>
</span></span><span style="display:flex;"><span>                   <span style="color:#a6e22e">translatable=</span><span style="color:#e6db74">&#34;yes&#34;</span><span style="color:#f92672">&gt;</span>_About Gazette<span style="color:#f92672">&lt;/attribute&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;attribute</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;action&#34;</span><span style="color:#f92672">&gt;</span>app.about<span style="color:#f92672">&lt;/attribute&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;/item&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/section&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/menu&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/interface&gt;</span>
</span></span></code></pre></div><p>This is GTK&rsquo;s XML-based UI definition format.</p>
<p>The <strong><code>&lt;template&gt;</code></strong> element defines a composite template — it&rsquo;s bound to the <code>GazetteWindow</code> class we defined in Rust. The <code>parent</code> attribute must match the <code>ParentType</code> in our <code>ObjectSubclass</code> implementation.</p>
<p>The widget hierarchy is:</p>
<ul>
<li><strong><code>AdwToolbarView</code></strong> — a libadwaita container that manages header bars and content areas
<ul>
<li><strong><code>AdwHeaderBar</code></strong> (top) — the standard GNOME header bar with the window title and controls
<ul>
<li><strong><code>GtkMenuButton</code></strong> — the hamburger menu button in the top-right corner</li>
</ul>
</li>
<li><strong><code>GtkLabel</code></strong> (content) — our &ldquo;Hello, World!&rdquo; text</li>
</ul>
</li>
</ul>
<p>The <strong><code>&lt;menu&gt;</code></strong> element defines the hamburger menu model. Each item has a label (the <code>_</code> prefix marks the keyboard accelerator character) and an action name. The <code>app.about</code> entry maps back to the action we defined in <code>application.rs</code>. The <code>app.preferences</code> action isn&rsquo;t connected yet — clicking it does nothing. The <code>app.shortcuts</code> action, however, already works: GTK automatically loads <code>shortcuts-dialog.ui</code> from the resource base path we set in <code>application.rs</code>. We&rsquo;ll wire up preferences in a later post.</p>
<p>The <code>translatable=&quot;yes&quot;</code> attribute on strings marks them for extraction by gettext. Even if you don&rsquo;t plan to translate your app initially, marking strings as translatable from the start saves you from a painful retrofit later.</p>
<p>In Part 4, we&rsquo;ll convert this XML template to Blueprint, which is dramatically more readable. But it&rsquo;s worth understanding the XML format first, since Blueprint compiles to it — and when things go wrong, the error messages reference XML structures.</p>
<hr>
<h2 id="the-build-system">The build system</h2>
<p>GNOME apps don&rsquo;t use Cargo as the top-level build system. They use <strong>Meson</strong>. Cargo still compiles the Rust code, but Meson orchestrates everything else — and there&rsquo;s a lot of &ldquo;everything else&rdquo; in a GNOME app.</p>
<h3 id="root-mesonbuild">Root meson.build</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-meson" data-lang="meson"><span style="display:flex;"><span>project(<span style="color:#e6db74">&#39;gazette&#39;</span>, <span style="color:#e6db74">&#39;rust&#39;</span>,
</span></span><span style="display:flex;"><span>          version: <span style="color:#e6db74">&#39;0.1.0&#39;</span>,
</span></span><span style="display:flex;"><span>    meson_version: <span style="color:#e6db74">&#39;&gt;= 1.0.0&#39;</span>,
</span></span><span style="display:flex;"><span>  default_options: [ <span style="color:#e6db74">&#39;warning_level=2&#39;</span>, <span style="color:#e6db74">&#39;werror=false&#39;</span>, ],
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>i18n <span style="color:#f92672">=</span> import(<span style="color:#e6db74">&#39;i18n&#39;</span>)
</span></span><span style="display:flex;"><span>gnome <span style="color:#f92672">=</span> import(<span style="color:#e6db74">&#39;gnome&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>subdir(<span style="color:#e6db74">&#39;data&#39;</span>)
</span></span><span style="display:flex;"><span>subdir(<span style="color:#e6db74">&#39;src&#39;</span>)
</span></span><span style="display:flex;"><span>subdir(<span style="color:#e6db74">&#39;po&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>gnome.post_install(
</span></span><span style="display:flex;"><span>     glib_compile_schemas: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    gtk_update_icon_cache: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  update_desktop_database: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The root build file declares the project, imports the <code>i18n</code> and <code>gnome</code> Meson modules, and includes three subdirectories. The <code>post_install</code> block runs after installation to compile GSettings schemas, update the icon cache, and refresh the desktop file database. These are standard GNOME post-install steps — without them, your app&rsquo;s icon won&rsquo;t appear in the application launcher and your settings won&rsquo;t be accessible.</p>
<h3 id="srcmesonbuild--where-rust-meets-meson">src/meson.build — where Rust meets Meson</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-meson" data-lang="meson"><span style="display:flex;"><span>pkgdatadir <span style="color:#f92672">=</span> get_option(<span style="color:#e6db74">&#39;prefix&#39;</span>) <span style="color:#960050;background-color:#1e0010">/</span> get_option(<span style="color:#e6db74">&#39;datadir&#39;</span>) <span style="color:#960050;background-color:#1e0010">/</span> meson.project_name()
</span></span><span style="display:flex;"><span>gnome <span style="color:#f92672">=</span> import(<span style="color:#e6db74">&#39;gnome&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>gnome.compile_resources(<span style="color:#e6db74">&#39;gazette&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#39;gazette.gresource.xml&#39;</span>,
</span></span><span style="display:flex;"><span>  gresource_bundle: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  install: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  install_dir: pkgdatadir,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>conf <span style="color:#f92672">=</span> configuration_data()
</span></span><span style="display:flex;"><span>conf.set_quoted(<span style="color:#e6db74">&#39;VERSION&#39;</span>, meson.project_version())
</span></span><span style="display:flex;"><span>conf.set_quoted(<span style="color:#e6db74">&#39;GETTEXT_PACKAGE&#39;</span>, <span style="color:#e6db74">&#39;gazette&#39;</span>)
</span></span><span style="display:flex;"><span>conf.set_quoted(<span style="color:#e6db74">&#39;LOCALEDIR&#39;</span>, get_option(<span style="color:#e6db74">&#39;prefix&#39;</span>) <span style="color:#960050;background-color:#1e0010">/</span> get_option(<span style="color:#e6db74">&#39;localedir&#39;</span>))
</span></span><span style="display:flex;"><span>conf.set_quoted(<span style="color:#e6db74">&#39;PKGDATADIR&#39;</span>, pkgdatadir)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>configure_file(
</span></span><span style="display:flex;"><span>    input: <span style="color:#e6db74">&#39;config.rs.in&#39;</span>,
</span></span><span style="display:flex;"><span>    output: <span style="color:#e6db74">&#39;config.rs&#39;</span>,
</span></span><span style="display:flex;"><span>    configuration: conf
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Here&rsquo;s what it handles:</p>
<ol>
<li>
<p><strong>Compiles GResources.</strong> The <code>gazette.gresource.xml</code> manifest lists the UI files and other assets that should be bundled. Meson compiles them into a single binary blob (<code>gazette.gresource</code>) and installs it alongside the binary.</p>
</li>
<li>
<p><strong>Generates <code>config.rs</code>.</strong> The <code>config.rs.in</code> template contains placeholders like <code>@VERSION@</code> and <code>@PKGDATADIR@</code>. Meson replaces them with real values at build time. This is how the Rust code knows where to find its installed resources — the paths are baked in at compile time.</p>
</li>
<li>
<p><strong>Invokes Cargo.</strong> Further down in the file, Meson runs <code>cargo build</code> with the right flags and copies the resulting binary to the install location.</p>
</li>
</ol>
<h3 id="gazettegresourcexml">gazette.gresource.xml</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;gresources&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;gresource</span> <span style="color:#a6e22e">prefix=</span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;file</span> <span style="color:#a6e22e">preprocess=</span><span style="color:#e6db74">&#34;xml-stripblanks&#34;</span><span style="color:#f92672">&gt;</span>window.ui<span style="color:#f92672">&lt;/file&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;file</span> <span style="color:#a6e22e">preprocess=</span><span style="color:#e6db74">&#34;xml-stripblanks&#34;</span><span style="color:#f92672">&gt;</span>shortcuts-dialog.ui<span style="color:#f92672">&lt;/file&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/gresource&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/gresources&gt;</span>
</span></span></code></pre></div><p>This manifest lists every file that should be compiled into the GResource bundle. The <code>prefix</code> attribute defines the virtual path — when our window template asks for <code>/io/github/fromthearchitect/gazette/window.ui</code>, it&rsquo;s loading from this bundle, not from the filesystem. The <code>xml-stripblanks</code> flag tells Meson to strip whitespace from the XML before bundling. It makes no real difference for an app this size, but it&rsquo;s standard practice and every GNOME app does it.</p>
<p>Whenever you add a new UI file, icon, or CSS stylesheet, it needs to be listed here.</p>
<hr>
<h2 id="desktop-integration-files">Desktop integration files</h2>
<p>The <code>data/</code> directory contains files that integrate your application with the Linux desktop. None of these are specific to GNOME or GTK — they&rsquo;re freedesktop.org standards used across all Linux desktop environments.</p>
<h3 id="desktop-file">Desktop file</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Desktop Entry]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Name</span><span style="color:#f92672">=</span><span style="color:#e6db74">Gazette</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Exec</span><span style="color:#f92672">=</span><span style="color:#e6db74">gazette</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Icon</span><span style="color:#f92672">=</span><span style="color:#e6db74">io.github.fromthearchitect.gazette</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Terminal</span><span style="color:#f92672">=</span><span style="color:#e6db74">false</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">Application</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Categories</span><span style="color:#f92672">=</span><span style="color:#e6db74">Utility;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Keywords</span><span style="color:#f92672">=</span><span style="color:#e6db74">GTK;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">StartupNotify</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">DBusActivatable</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
</span></span></code></pre></div><p>This tells the desktop environment how to launch your app, what icon to display, and what category to put it in. The <code>.in</code> suffix on the source file means Meson processes it through gettext to produce translated versions.</p>
<p><code>DBusActivatable=true</code> means the app can be launched via D-Bus, which enables the single-instance behaviour we saw in <code>application.rs</code>.</p>
<h3 id="gsettings-schema">GSettings schema</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;schemalist</span> <span style="color:#a6e22e">gettext-domain=</span><span style="color:#e6db74">&#34;gazette&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;schema</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;io.github.fromthearchitect.gazette&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">path=</span><span style="color:#e6db74">&#34;/io/github/fromthearchitect/gazette/&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/schema&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/schemalist&gt;</span>
</span></span></code></pre></div><p>GSettings is GNOME&rsquo;s configuration system — think of it as a typed key-value store for application preferences. The schema is empty now, but when we add user preferences (feed refresh interval, dark mode preference, etc.), this is where we&rsquo;ll define them.</p>
<h3 id="appstream-metadata">AppStream metadata</h3>
<p>The metainfo file is what Flathub and GNOME Software actually read when they display your app. Get it wrong and your app looks abandoned regardless of how polished the code is. Builder generated a placeholder — we&rsquo;ll fill it in properly when we prepare for Flathub submission in Part 9.</p>
<h3 id="d-bus-service-file">D-Bus service file</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[D-BUS Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Name</span><span style="color:#f92672">=</span><span style="color:#e6db74">io.github.fromthearchitect.gazette</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Exec</span><span style="color:#f92672">=</span><span style="color:#e6db74">@bindir@/gazette --gapplication-service</span>
</span></span></code></pre></div><p>This tells D-Bus how to activate your application on demand. The <code>@bindir@</code> placeholder is replaced by Meson at build time with the actual installation path.</p>
<hr>
<h2 id="the-flatpak-manifest">The Flatpak manifest</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;id&#34;</span> : <span style="color:#e6db74">&#34;io.github.fromthearchitect.gazette&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;runtime&#34;</span> : <span style="color:#e6db74">&#34;org.gnome.Platform&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;runtime-version&#34;</span> : <span style="color:#e6db74">&#34;48&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;sdk&#34;</span> : <span style="color:#e6db74">&#34;org.gnome.Sdk&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;sdk-extensions&#34;</span> : [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;org.freedesktop.Sdk.Extension.rust-stable&#34;</span>
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;command&#34;</span> : <span style="color:#e6db74">&#34;gazette&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;finish-args&#34;</span> : [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;--share=network&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;--share=ipc&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;--socket=fallback-x11&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;--device=dri&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;--socket=wayland&#34;</span>
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#960050;background-color:#1e0010">...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This defines how to build and sandbox the application as a Flatpak. The key fields:</p>
<ul>
<li><strong><code>runtime</code></strong> and <strong><code>runtime-version</code></strong>: The GNOME platform libraries your app links against at runtime. Version 48 corresponds to GNOME 48.</li>
<li><strong><code>sdk</code></strong>: The matching SDK used at build time, which includes header files and development tools.</li>
<li><strong><code>sdk-extensions</code></strong>: Additional SDK components — here, the Rust toolchain.</li>
<li><strong><code>finish-args</code></strong>: The sandbox permissions your app needs. Network access, Wayland display, GPU acceleration. Every permission you add here needs to be justified when you submit to Flathub. You might wonder why an RSS reader needs <code>--device=dri</code> — GTK4 uses GPU-accelerated rendering by default, so it&rsquo;s a legitimate permission for virtually any GTK4 app.</li>
</ul>
<hr>
<h2 id="upgrading-to-the-gnome-50-platform">Upgrading to the GNOME 50 platform</h2>
<p>Builder generated this project against <strong>GNOME 48</strong> — that&rsquo;s what the current template targets. But in <a href="/posts/gnome-rust-part-1-getting-started/">Part 1</a>, we installed the <strong>GNOME 50 SDK</strong>. Let&rsquo;s upgrade the Rust crates and Flatpak runtime to match.</p>
<p>Three files need changes:</p>
<h3 id="1-cargotoml--rust-crate-versions">1. Cargo.toml — Rust crate versions</h3>
<p>The generated <code>Cargo.toml</code> uses <code>gtk4</code> 0.9 and <code>libadwaita</code> 0.7:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">dependencies</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">gettext-rs</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.7&#34;</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;gettext-system&#34;</span>] }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">gtk</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.9&#34;</span>, <span style="color:#a6e22e">package</span> = <span style="color:#e6db74">&#34;gtk4&#34;</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;gnome_47&#34;</span>] }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">dependencies</span>.<span style="color:#a6e22e">adw</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">package</span> = <span style="color:#e6db74">&#34;libadwaita&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.7&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;v1_6&#34;</span>]
</span></span></code></pre></div><p>Update it to the latest versions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">dependencies</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">gettext-rs</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.7&#34;</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;gettext-system&#34;</span>] }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">gtk</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.11&#34;</span>, <span style="color:#a6e22e">package</span> = <span style="color:#e6db74">&#34;gtk4&#34;</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;gnome_50&#34;</span>] }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">adw</span> = { <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.9&#34;</span>, <span style="color:#a6e22e">package</span> = <span style="color:#e6db74">&#34;libadwaita&#34;</span>, <span style="color:#a6e22e">features</span> = [<span style="color:#e6db74">&#34;v1_9&#34;</span>] }
</span></span></code></pre></div><p>The version numbers here are the Rust crate versions, not the GTK version. <code>gtk4</code> crate 0.11 wraps GTK 4.18, and <code>libadwaita</code> crate 0.9 wraps libadwaita 1.9. The <code>gnome_50</code> feature flag is a convenience flag that enables all the version-specific API features matching the GNOME 50 SDK — it saves you from having to figure out which individual <code>v4_xx</code> flags correspond to which GNOME release.</p>
<h3 id="2-flatpak-manifest--runtime-version">2. Flatpak manifest — runtime version</h3>
<p>In <code>io.github.fromthearchitect.gazette.json</code>, update the runtime version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;runtime-version&#34;</span> : <span style="color:#e6db74">&#34;50&#34;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This tells Flatpak to use the GNOME 50 runtime and SDK when building and running your app.</p>
<h3 id="3-rebuild">3. Rebuild</h3>
<p>Hit <strong>Run</strong> in Builder again. Builder will download the GNOME 50 SDK if it&rsquo;s not already installed, rebuild the project against the new dependencies, and launch the app. If everything compiles and runs — and it should, since we haven&rsquo;t used any version-specific APIs yet — you&rsquo;re on the latest platform.</p>
<p>This kind of version bump is something you&rsquo;ll do roughly every six months as GNOME releases new platform versions. Getting comfortable with it now means it&rsquo;s routine rather than stressful later.</p>
<hr>
<h2 id="what-we-have-so-far">What we have so far</h2>
<p>The thing that takes longest to internalise when you start GNOME development is how much of this is not GTK. The desktop file, the D-Bus service, the AppStream metadata, GSettings — these are platform concerns, not application concerns. Builder stitches them all together, but you need to know what they are and why they exist, or you&rsquo;ll never debug a broken Flatpak submission.</p>
<p>The application ID is the load-bearing string across all of it. It ties the desktop file to the D-Bus service to the GSettings schema to the Flatpak bundle. Getting this scaffolding right by hand would take hours of cross-referencing documentation. Builder gives it to you in thirty seconds.</p>
<hr>
<h2 id="what-comes-next">What comes next</h2>
<p>We&rsquo;ve got a running app, but the UI is still &ldquo;Hello, World!&rdquo;. Before we start building the actual RSS reader interface, we need a better way to describe user interfaces than raw XML.</p>
<p>In Part 4, we&rsquo;ll introduce Blueprint — a markup language that compiles to GTK&rsquo;s UI XML but is dramatically more readable. We&rsquo;ll convert the generated window template to Blueprint, build the initial Gazette layout with a sidebar and content pane, and start making it look like a real application.</p>
<hr>
<p>The source code at the end of this post lives on the <code>part-3</code> branch of <a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-3" target="_blank" rel="noopener">fromthearchitect/gnome-rust-gazette</a>. Subsequent posts in this series build on the same repo, with a branch per part.</p>
]]></content:encoded></item><item><title>Building GNOME Apps with Rust, Part 2: GObject in Rust — The Type System Explained</title><link>https://fromthearchitect.dev/posts/gnome-rust-part-2-gobject/?utm_source=rss</link><pubDate>Sun, 12 Apr 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-part-2-gobject/</guid><description>Before we build a GNOME app, we need to understand GObject — the type system underneath everything in GTK. Here&amp;#39;s how it works in Rust.</description><content:encoded><![CDATA[<p><em>This is Part 2 of a series taking a GNOME app from an empty directory to GNOME Circle. <a href="/posts/gnome-rust-part-1-getting-started/">Part 1</a> covered the why and the dev environment.</em></p>
<hr>
<h2 id="the-app-were-building">The app we&rsquo;re building</h2>
<p>Throughout this series, we&rsquo;ll be building <strong>Gazette</strong> — an RSS reader. I picked it because it naturally exercises every pattern a non-trivial GNOME app needs: networking, data persistence, list/detail UI, adaptive layouts, settings, and state management. You don&rsquo;t need to build an RSS reader to follow along — the patterns are universal.</p>
<p>But before we touch GTK, before we create a window or a header bar, we need to understand the type system that everything in GNOME is built on — GObject. Every hour you spend on it before touching widgets will save you two hours of confusion when your property binding silently doesn&rsquo;t work or your signal callback never fires.</p>
<hr>
<h2 id="what-is-gobject-and-why-should-you-care">What is GObject and why should you care</h2>
<p>Every widget in GTK is a GObject. That sounds like a detail you can safely skip. It isn&rsquo;t — it explains why property bindings work the way they do, why signals don&rsquo;t need explicit subscriber lists, and why half the confusion I see in gtk-rs questions comes from not knowing this layer exists.</p>
<p>GObject gives C what it was never designed to have: objects. You get inheritance, properties that can be observed and bound, a signal system for decoupled events, and reference counting. All achieved through C macros and conventions, which is exactly as readable as it sounds.</p>
<p>The challenge for Rust developers is that these two type systems have fundamentally different ideas about ownership. Rust doesn&rsquo;t have inheritance. GObject is built on it. Rust tracks lifetimes at compile time. GObject uses runtime reference counting. The gtk-rs bindings bridge this gap with a specific pattern — and once you&rsquo;ve seen it once, you&rsquo;ll recognise it everywhere.</p>
<hr>
<h2 id="a-simple-gobject-the-feed-model">A simple GObject: the Feed model</h2>
<p>We&rsquo;re going to create a <code>Feed</code> type — an RSS feed with a title, a URL, and an unread count. No widgets, no UI. Just a plain GObject that holds data, exposes properties, and emits a signal. This is the simplest way to see every piece of the pattern without distractions.</p>
<h3 id="setting-up">Setting up</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cargo init gobject-example
</span></span><span style="display:flex;"><span>cd gobject-example
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">package</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;gobject-example&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.1.0&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">edition</span> = <span style="color:#e6db74">&#34;2021&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">dependencies</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">gtk</span> = { <span style="color:#a6e22e">package</span> = <span style="color:#e6db74">&#34;gtk4&#34;</span>, <span style="color:#a6e22e">version</span> = <span style="color:#e6db74">&#34;0.11&#34;</span> }
</span></span></code></pre></div><p>We only need gtk4 — it exposes <code>glib</code> and <code>gio</code> as <code>gtk::glib</code> and <code>gtk::gio</code>. No libadwaita for this.</p>
<hr>
<h2 id="the-innerouter-type-split">The inner/outer type split</h2>
<p>Every custom GObject in Rust is two types: an inner type that lives in <code>mod imp</code> and holds the actual state, and a lightweight outer type that&rsquo;s what your code actually touches. GObject owns the inner; the outer is just a reference-counted handle to it.</p>
<p>This split exists because both GObject and Rust want to own the data. GObject manages lifecycles through reference counting. Rust enforces ownership at compile time. The inner/outer pattern resolves the tension — GObject gets ownership of the inner type, and you get a cheaply-cloneable handle to pass around. If you&rsquo;ve used <code>Rc&lt;RefCell&lt;T&gt;&gt;</code>, the mental model is similar — though GObject&rsquo;s ref count is atomic, so <code>Arc&lt;RefCell&lt;T&gt;&gt;</code> is closer to the truth. The atomicity is about safe counting, not thread-safe access; GTK objects still need to stay on the main thread.</p>
<hr>
<h2 id="the-inner-type">The inner type</h2>
<p>Create <code>src/feed.rs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> imp {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> std::cell::{Cell, RefCell};
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> std::sync::OnceLock;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> glib::subclass::Signal;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> glib::Properties;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> gtk::glib;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">use</span> gtk::subclass::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[derive(Properties, Default)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[properties(wrapper_type = super::Feed)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Feed</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>        title: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>        url: <span style="color:#a6e22e">RefCell</span><span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">#[property(get, set)]</span>
</span></span><span style="display:flex;"><span>        unread_count: <span style="color:#a6e22e">Cell</span><span style="color:#f92672">&lt;</span><span style="color:#66d9ef">u32</span><span style="color:#f92672">&gt;</span>,
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[glib::object_subclass]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectSubclass <span style="color:#66d9ef">for</span> Feed {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">NAME</span>: <span style="color:#66d9ef">&amp;</span>&#39;static <span style="color:#66d9ef">str</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;GazetteFeed&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Type</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">super</span>::Feed;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[glib::derived_properties]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">impl</span> ObjectImpl <span style="color:#66d9ef">for</span> Feed {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">signals</span>() -&gt; <span style="color:#66d9ef">&amp;</span>&#39;static [Signal] {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">SIGNALS</span>: <span style="color:#a6e22e">OnceLock</span><span style="color:#f92672">&lt;</span>Vec<span style="color:#f92672">&lt;</span>Signal<span style="color:#f92672">&gt;&gt;</span> <span style="color:#f92672">=</span> OnceLock::new();
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">SIGNALS</span>.get_or_init(<span style="color:#f92672">||</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">vec!</span>[Signal::builder(<span style="color:#e6db74">&#34;items-updated&#34;</span>)
</span></span><span style="display:flex;"><span>                    .param_types([<span style="color:#66d9ef">u32</span>::static_type()])
</span></span><span style="display:flex;"><span>                    .build()]
</span></span><span style="display:flex;"><span>            })
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Let&rsquo;s unpack the important parts.</p>
<p><strong>Properties and interior mutability.</strong> Each field marked <code>#[property(get, set)]</code> becomes a GObject property — accessible by string name, bindable, observable. The <code>RefCell&lt;String&gt;</code> and <code>Cell&lt;u32&gt;</code> types are required because GObject&rsquo;s API is <code>&amp;self</code> everywhere, never <code>&amp;mut self</code>. Use <code>Cell&lt;T&gt;</code> for <code>Copy</code> types, <code>RefCell&lt;T&gt;</code> for everything else.</p>
<p><strong>ObjectSubclass.</strong> This registers the type with GObject. <code>NAME</code> must be globally unique — duplicates cause a runtime panic. We don&rsquo;t set <code>ParentType</code> because the default is <code>glib::Object</code>. When we get to widgets, we&rsquo;ll set it explicitly.</p>
<p><strong>ObjectImpl and signals.</strong> The <code>#[glib::derived_properties]</code> attribute wires up the getters and setters. The <code>signals</code> method defines a custom <code>items-updated</code> signal that carries a <code>u32</code>. Signals are GObject&rsquo;s event system — listeners connect without the emitter knowing who&rsquo;s listening.</p>
<hr>
<h2 id="the-outer-type">The outer type</h2>
<p>Still in <code>src/feed.rs</code>, below <code>mod imp</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> glib::Object;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::glib;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>glib::<span style="color:#a6e22e">wrapper!</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">struct</span> <span style="color:#a6e22e">Feed</span>(ObjectSubclass<span style="color:#f92672">&lt;</span>imp::Feed<span style="color:#f92672">&gt;</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">impl</span> Feed {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">new</span>(title: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>, url: <span style="color:#66d9ef">&amp;</span><span style="color:#66d9ef">str</span>) -&gt; <span style="color:#a6e22e">Self</span> {
</span></span><span style="display:flex;"><span>        Object::builder()
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;title&#34;</span>, title)
</span></span><span style="display:flex;"><span>            .property(<span style="color:#e6db74">&#34;url&#34;</span>, url)
</span></span><span style="display:flex;"><span>            .build()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">pub</span> <span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">mark_items_updated</span>(<span style="color:#f92672">&amp;</span>self, new_count: <span style="color:#66d9ef">u32</span>) {
</span></span><span style="display:flex;"><span>        self.set_unread_count(new_count);
</span></span><span style="display:flex;"><span>        self.emit_by_name::<span style="color:#f92672">&lt;</span>()<span style="color:#f92672">&gt;</span>(<span style="color:#e6db74">&#34;items-updated&#34;</span>, <span style="color:#f92672">&amp;</span>[<span style="color:#f92672">&amp;</span>new_count]);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>glib::wrapper!</code> macro generates a thin, reference-counted pointer to the inner type. Clone it freely — it just bumps the ref count.</p>
<p>GObjects are built through the builder pattern, setting properties by name. <code>mark_items_updated</code> deliberately sets the property <em>and</em> emits the signal as a single operation — in a real app, you don&rsquo;t want callers updating the count without notifying listeners.</p>
<hr>
<h2 id="using-it">Using it</h2>
<p>In <code>src/main.rs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">mod</span> feed;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> feed::Feed;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::glib;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> gtk::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> feed <span style="color:#f92672">=</span> Feed::new(<span style="color:#e6db74">&#34;GNOME Planet&#34;</span>, <span style="color:#e6db74">&#34;https://planet.gnome.org/atom.xml&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Read properties
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;Feed: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74"> (</span><span style="color:#e6db74">{}</span><span style="color:#e6db74">)&#34;</span>, feed.title(), feed.url());
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;Unread: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, feed.unread_count());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Connect to the signal
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    feed.connect_closure(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;items-updated&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>        glib::<span style="color:#a6e22e">closure_local!</span>(<span style="color:#f92672">|</span>_feed: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">Feed</span>, count: <span style="color:#66d9ef">u32</span><span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;Feed updated — </span><span style="color:#e6db74">{}</span><span style="color:#e6db74"> new items&#34;</span>, count);
</span></span><span style="display:flex;"><span>        }),
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Connect to property change notifications
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    feed.connect_notify(Some(<span style="color:#e6db74">&#34;unread-count&#34;</span>), <span style="color:#f92672">|</span>feed, _<span style="color:#f92672">|</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;Unread count changed to </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, feed.unread_count());
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Update the feed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    feed.mark_items_updated(<span style="color:#ae81ff">5</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Property binding — one Feed mirroring another&#39;s unread count
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">let</span> mirror <span style="color:#f92672">=</span> Feed::new(<span style="color:#e6db74">&#34;Mirror&#34;</span>, <span style="color:#e6db74">&#34;https://example.com/feed.xml&#34;</span>);
</span></span><span style="display:flex;"><span>    feed.bind_property(<span style="color:#e6db74">&#34;unread-count&#34;</span>, <span style="color:#f92672">&amp;</span>mirror, <span style="color:#e6db74">&#34;unread-count&#34;</span>)
</span></span><span style="display:flex;"><span>        .sync_create()
</span></span><span style="display:flex;"><span>        .build();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;Mirror unread: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, mirror.unread_count()); <span style="color:#75715e">// 5
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>    feed.mark_items_updated(<span style="color:#ae81ff">12</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">println!</span>(<span style="color:#e6db74">&#34;Mirror unread: </span><span style="color:#e6db74">{}</span><span style="color:#e6db74">&#34;</span>, mirror.unread_count()); <span style="color:#75715e">// 12
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>Feed: GNOME Planet (https://planet.gnome.org/atom.xml)
</span></span><span style="display:flex;"><span>Unread: 0
</span></span><span style="display:flex;"><span>Unread count changed to 5
</span></span><span style="display:flex;"><span>Feed updated — 5 new items
</span></span><span style="display:flex;"><span>Mirror unread: 5
</span></span><span style="display:flex;"><span>Unread count changed to 12
</span></span><span style="display:flex;"><span>Feed updated — 12 new items
</span></span><span style="display:flex;"><span>Mirror unread: 12
</span></span></code></pre></div><p>No <code>gtk::init()</code> call — we&rsquo;re only using GLib objects here, no widgets, no event loop. Synchronous signal emission works fine without one; once you&rsquo;re dispatching across async boundaries, you&rsquo;ll need a running main loop.</p>
<h3 id="property-binding-is-the-one-worth-sitting-with">Property binding is the one worth sitting with</h3>
<p>Properties, signals, notifications — you wire those up manually. Binding is different. It&rsquo;s declarative: state a relationship once and the type system enforces it. When we build Gazette&rsquo;s UI, this is how we&rsquo;ll connect models to widgets — a property changes on a model and the bound widget updates automatically, no event handler code. The first time that happens, you stop thinking in callbacks. That&rsquo;s the shift.</p>
<hr>
<h2 id="the-pattern-summarised">The pattern, summarised</h2>
<p>Every custom GObject in Rust follows this structure:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-gdscript3" data-lang="gdscript3"><span style="display:flex;"><span>src<span style="color:#f92672">/</span>my_type<span style="color:#f92672">.</span>rs
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">├──</span> mod imp {
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">│</span>   <span style="color:#960050;background-color:#1e0010">├──</span> struct MyType { <span style="color:#f92672">...</span> }           <span style="color:#f92672">//</span> State (with Cell<span style="color:#f92672">/</span>RefCell)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">│</span>   <span style="color:#960050;background-color:#1e0010">├──</span> impl ObjectSubclass <span style="color:#66d9ef">for</span> MyType  <span style="color:#f92672">//</span> Registration (NAME, Type, ParentType)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">│</span>   <span style="color:#960050;background-color:#1e0010">└──</span> impl ObjectImpl <span style="color:#66d9ef">for</span> MyType      <span style="color:#f92672">//</span> Lifecycle (properties, signals)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">│</span>   <span style="color:#960050;background-color:#1e0010">└──</span> impl WidgetImpl, etc<span style="color:#f92672">.</span>           <span style="color:#f92672">//</span> Parent traits (only <span style="color:#66d9ef">for</span> widgets)
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">│</span>   }
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">├──</span> glib::wrapper<span style="color:#f92672">!</span> { <span style="color:#f92672">...</span> }             <span style="color:#f92672">//</span> Outer type declaration
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">└──</span> impl MyType { <span style="color:#f92672">...</span> }                <span style="color:#f92672">//</span> Public API
</span></span></code></pre></div><p>Yes, it&rsquo;s a lot of ceremony for a type that holds three fields. That&rsquo;s the price of entry — GObject was designed for C, and bridging two type systems isn&rsquo;t free. But the payoff is that every GObject you write after this one is the same shape. You stop thinking about the structure and start thinking about what the type actually does. The boilerplate becomes invisible by the third or fourth time.</p>
<hr>
<h2 id="common-mistakes">Common mistakes</h2>
<p><strong>Forgetting interior mutability</strong> — <code>title: String</code> instead of <code>title: RefCell&lt;String&gt;</code> will fail at compile time. GObject gives you <code>&amp;self</code>, never <code>&amp;mut self</code>.</p>
<p><strong>Duplicate <code>NAME</code> strings</strong> — runtime panic, not a compile error. Namespace them: <code>GazetteFeed</code>, not <code>Feed</code>.</p>
<p><strong>Property name casing</strong> — GObject properties use kebab-case (<code>unread-count</code>), Rust methods use snake_case (<code>unread_count()</code>). When referencing by string — <code>connect_notify</code>, <code>bind_property</code>, <code>Object::builder()</code> — use kebab-case. Get it wrong and it silently fails.</p>
<p><strong>Forgetting <code>#[glib::derived_properties]</code></strong> — without it, the <code>Properties</code> derive generates Rust methods but the GObject type system doesn&rsquo;t know about them. Bindings and notifications won&rsquo;t work.</p>
<hr>
<h2 id="what-comes-next">What comes next</h2>
<p>Now that we understand GObject&rsquo;s type system — properties, signals, the inner/outer split — we&rsquo;re ready to apply it. In Part 3, we&rsquo;ll build Gazette&rsquo;s application skeleton: an <code>adw::Application</code>, an <code>adw::ApplicationWindow</code>, a header bar, and a Blueprint template. The GObject pattern is exactly the same; the trait chain is just longer.</p>
<hr>
<p>The source code for this post is available on <a href="https://github.com/fromthearchitect/gnome-rust-part-2-gobject" target="_blank" rel="noopener">GitHub</a>.</p>
]]></content:encoded></item><item><title>Building GNOME Apps with Rust, Part 1: Getting Started</title><link>https://fromthearchitect.dev/posts/gnome-rust-part-1-getting-started/?utm_source=rss</link><pubDate>Mon, 06 Apr 2026 00:00:00 +1000</pubDate><guid>https://fromthearchitect.dev/posts/gnome-rust-part-1-getting-started/</guid><description>Why GNOME, why Rust, and how to set up a development environment that will carry you from first widget to Flathub submission</description><content:encoded><![CDATA[<p><em>This is Part 1 of a series that takes a GNOME application from an empty directory to acceptance into GNOME Circle. Each post is self-contained, but the series follows a single arc — and a real app — through every stage of the journey.</em></p>
<hr>
<h2 id="why-gnome">Why GNOME</h2>
<p>If you&rsquo;re building a desktop Linux application in 2026, you&rsquo;ve got choices. KDE Plasma has Kirigami. Elementary has Granite. You can reach for Electron, Tauri, or a dozen other cross-platform toolkits and call it a day.</p>
<p>I chose GNOME because of what it feels like to use. The GNOME desktop provides a clean, distraction-free approach to computing. Its consistency rivals early macOS — there&rsquo;s a feeling that everything is in its place. When I started building Moments, my photo management app, that consistency carried over into the development experience in a way I didn&rsquo;t expect. The <a href="https://developer.gnome.org/hig/" target="_blank" rel="noopener">GNOME Human Interface Guidelines</a> aren&rsquo;t suggestions — they&rsquo;re a design language. Follow them and your app inherits a coherent visual identity, consistent interaction patterns, and accessibility support you didn&rsquo;t have to design yourself. Your header bar looks right. Your adaptive layout works the way users expect. Your keyboard navigation is correct.</p>
<p>That matters more than it sounds like it should. Users develop muscle memory around their environment. An app that respects that muscle memory earns trust immediately. An app that doesn&rsquo;t — even if it&rsquo;s technically superior — creates friction. GNOME gives you a head start on eliminating that friction, but only if you actually use the toolkit the way it&rsquo;s meant to be used.</p>
<p>The ecosystem is also unusually healthy for an open source desktop project. <a href="https://flathub.org/" target="_blank" rel="noopener">Flathub</a> provides distribution. <a href="https://circle.gnome.org/" target="_blank" rel="noopener">GNOME Circle</a> provides recognition, infrastructure, and community. The tooling — Builder, Workbench, Cambalache — is actively maintained and improving. And the community, while not enormous, is engaged and welcoming in a way that larger ecosystems sometimes aren&rsquo;t.</p>
<hr>
<h2 id="why-rust">Why Rust</h2>
<p>GNOME has traditionally been a C ecosystem. GTK is written in C. GLib is written in C. The GObject type system — which underpins everything — is C&rsquo;s answer to object-oriented programming, complete with manual reference counting and a macro-heavy convention system that works remarkably well once you&rsquo;ve internalised it.</p>
<p>You don&rsquo;t need to write C to build GNOME apps. The GObject Introspection system generates bindings for dozens of languages, and three in particular have strong GNOME stories: Python with PyGObject, Vala (a language designed specifically for GObject development), and Rust with gtk-rs.</p>
<p>I&rsquo;m writing this series in Rust, and I think it&rsquo;s the right default choice for new GNOME apps in 2026.</p>
<p><strong>The ecosystem has moved.</strong> Look at recent GNOME Circle apps — Fractal, Switcheroo, Hieroglyphic — the Rust cohort is growing fast. When you hit a problem building a Rust/GTK app, there&rsquo;s now a meaningful body of real-world code to reference. That wasn&rsquo;t true even two years ago.</p>
<p><strong>The gtk-rs bindings are mature.</strong> The gtk4-rs and libadwaita-rs crates provide safe, idiomatic Rust wrappers around the full GTK4 and libadwaita API surface. They handle reference counting, type casting, and signal connections in a way that feels natural in Rust. These aren&rsquo;t thin C bindings with unsafe blocks everywhere — they&rsquo;re a genuine Rust API for building GTK applications.</p>
<p><strong>Rust&rsquo;s strengths align with desktop app needs.</strong> Memory safety without garbage collection. Fearless concurrency. A type system that catches entire categories of bugs at compile time. These aren&rsquo;t just theoretical benefits — they show up in practice when you&rsquo;re managing complex UI state, handling async operations, and trying to ship software that doesn&rsquo;t crash.</p>
<p><strong>The tooling works.</strong> rust-analyzer provides excellent IDE support. Cargo handles dependency management. The Flatpak SDK includes a full Rust toolchain. You can get from <code>cargo init</code> to a running Flatpak application without fighting your tools.</p>
<p>There are tradeoffs. Rust has a steeper learning curve than Python, and the GObject subclassing pattern in Rust adds a layer of complexity that doesn&rsquo;t exist in Python or Vala. Compile times are slower. Iteration speed — especially building inside Flatpak — is meaningfully worse than Python&rsquo;s edit-and-run loop. These are real costs, and I won&rsquo;t pretend otherwise. But the learning curve is a curve, not a wall, and this series will walk through every part of it.</p>
<hr>
<h2 id="setting-up-the-development-environment">Setting up the development environment</h2>
<p>Before you write a single line of Rust or GTK code, you need three things: GNOME Builder, the Flatpak SDK, and a working understanding of why we&rsquo;re using Flatpak from day one.</p>
<h3 id="why-flatpak-from-the-start">Why Flatpak from the start</h3>
<p>Most development guides have you install GTK libraries on your host system, write code against them, and worry about packaging later. This works — until it doesn&rsquo;t. The failure mode is specific and painful: you develop against GTK 4.18 on your host, everything works, you go to package the app for Flathub six months later, and discover that the GNOME SDK version you need to target ships GTK 4.16, and three APIs you depend on don&rsquo;t exist.</p>
<p>Building inside Flatpak from the beginning eliminates this entire class of problem. Your development environment matches your deployment environment. The libraries you link against are the libraries your users will have. The dependencies are explicit and reproducible. When you submit to Flathub, there are no surprises.</p>
<p>It&rsquo;s slightly more friction up front. It&rsquo;s dramatically less friction in total.</p>
<h3 id="install-flatpak-and-flathub">Install Flatpak and Flathub</h3>
<p>If you&rsquo;re running Fedora, Flatpak is already configured. For other distributions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install Flatpak (if not present)</span>
</span></span><span style="display:flex;"><span>sudo apt install flatpak    <span style="color:#75715e"># Debian/Ubuntu</span>
</span></span><span style="display:flex;"><span>sudo pacman -S flatpak      <span style="color:#75715e"># Arch</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Add the Flathub remote</span>
</span></span><span style="display:flex;"><span>flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
</span></span></code></pre></div><h3 id="install-the-gnome-sdk">Install the GNOME SDK</h3>
<p>The GNOME SDK provides the full development environment — GTK4, libadwaita, GLib, and the Rust toolchain — inside a Flatpak runtime. You need both the runtime and the SDK, plus the Rust extension:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install the GNOME 50 SDK and runtime</span>
</span></span><span style="display:flex;"><span>flatpak install flathub org.gnome.Sdk//50 org.gnome.Platform//50
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Install the Rust extension for the SDK</span>
</span></span><span style="display:flex;"><span>flatpak install flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
</span></span></code></pre></div><p>A note on version numbers: the GNOME SDK version (50, at time of writing) corresponds to the GNOME release. The Rust extension version (25.08) corresponds to the freedesktop SDK version that the GNOME SDK is built on. These version numbers will change — check the <a href="https://docs.flathub.org/docs/for-app-authors/requirements" target="_blank" rel="noopener">Flathub runtime page</a> for current versions.</p>
<h3 id="install-gnome-builder">Install GNOME Builder</h3>
<p>GNOME Builder is the IDE purpose-built for GNOME development. It understands Meson, Flatpak manifests, and the GNOME SDK. It can build your application inside the Flatpak sandbox, run it, and provide code intelligence — all without you manually configuring build environments.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>flatpak install flathub org.gnome.Builder
</span></span></code></pre></div><p>Builder isn&rsquo;t the only option. You can use VS Code, Neovim, or any editor you prefer — I&rsquo;ll cover that setup in a later post on developer experience. But Builder eliminates the most configuration friction for getting started, and it&rsquo;s what I&rsquo;d recommend for your first project.</p>
<h3 id="verify-the-toolchain">Verify the toolchain</h3>
<p>Open a terminal inside the Flatpak SDK environment to confirm everything is in place:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Enter the SDK environment</span>
</span></span><span style="display:flex;"><span>flatpak run --command<span style="color:#f92672">=</span>bash org.gnome.Sdk//50
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inside the SDK shell:</span>
</span></span><span style="display:flex;"><span>rustc --version
</span></span><span style="display:flex;"><span>cargo --version
</span></span><span style="display:flex;"><span>pkg-config --libs gtk4
</span></span><span style="display:flex;"><span>pkg-config --libs libadwaita-1
</span></span></code></pre></div><p>You should see a recent stable Rust version and successful pkg-config output for both GTK4 and libadwaita. If any of these fail, the SDK or Rust extension didn&rsquo;t install correctly — reinstall them before continuing.</p>
<h3 id="a-note-on-host-development">A note on host development</h3>
<p>You can also develop on your host system by installing the GTK4 and libadwaita development packages directly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Fedora</span>
</span></span><span style="display:flex;"><span>sudo dnf install gtk4-devel libadwaita-devel
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Arch</span>
</span></span><span style="display:flex;"><span>sudo pacman -S gtk4 libadwaita
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Ubuntu/Debian (may lag behind on versions)</span>
</span></span><span style="display:flex;"><span>sudo apt install libgtk-4-dev libadwaita-1-dev
</span></span></code></pre></div><p>This gives you faster compile times and a tighter edit-compile-run loop, which matters during active development. Many Rust/GTK developers work this way day-to-day, using host-installed libraries for rapid iteration and Flatpak builds for testing and release.</p>
<p>The risk is version drift between your host libraries and the Flatpak SDK. If you go this route, check which GTK4 and libadwaita versions your host provides and compare them to the GNOME SDK you&rsquo;re targeting. As long as your host version is equal to or newer than the SDK version, you&rsquo;re fine. If it&rsquo;s older, you&rsquo;ll hit missing APIs.</p>
<p>My recommendation: start with Flatpak-only builds until you&rsquo;ve got a working app, then add host builds as an optimisation once you understand the version boundaries.</p>
<hr>
<h2 id="the-tools-you-will-be-using">The tools you will be using</h2>
<p>Before we start writing code in Part 2, here&rsquo;s a brief orientation to the tools that&rsquo;ll appear throughout this series. You don&rsquo;t need deep knowledge of any of them yet — just awareness that they exist and what role they play.</p>
<p><strong><a href="https://mesonbuild.com/" target="_blank" rel="noopener">Meson</a></strong> — The build system. GNOME apps use Meson, not Cargo, as the top-level build system. Cargo still builds your Rust code, but Meson orchestrates everything else: compiling GResources, installing desktop files, generating application metadata, and invoking Cargo as part of the build. Part 3 covers Meson in detail.</p>
<p><strong><a href="https://gnome.pages.gitlab.gnome.org/blueprint-compiler/" target="_blank" rel="noopener">Blueprint</a></strong> — A markup language for defining GTK user interfaces. Blueprint compiles to the XML that GTK&rsquo;s GtkBuilder expects, but it&rsquo;s dramatically more readable. You can also build your entire UI in Rust code without Blueprint — we&rsquo;ll cover both approaches.</p>
<p><strong><a href="https://docs.gtk.org/gio/struct.Resource.html" target="_blank" rel="noopener">GResource</a></strong> — GNOME&rsquo;s system for bundling assets (UI templates, icons, CSS) into your application binary. Instead of loading files from disk at runtime, GResources get compiled into the binary and accessed by path. It&rsquo;s how GNOME apps ship self-contained.</p>
<p><strong><a href="https://www.freedesktop.org/software/appstream/docs/" target="_blank" rel="noopener">Appstream</a></strong> — The metadata standard that app stores (including Flathub) use to display your app&rsquo;s name, description, screenshots, and release notes. You&rsquo;ll need to get this right for Flathub acceptance — Part 9 covers it.</p>
<p><strong><a href="https://apps.gnome.org/Workbench/" target="_blank" rel="noopener">Workbench</a></strong> — A playground app for experimenting with GTK widgets, CSS, and Blueprint templates. If you want to test a UI idea without rebuilding your whole app, Workbench is where you do it. Grab it from Flathub.</p>
<hr>
<h2 id="what-were-building">What we&rsquo;re building</h2>
<p>Part 2 opens with introducing the app we&rsquo;ll build throughout this series. It won&rsquo;t be a toy counter or a TODO list — it&rsquo;ll be something with enough complexity to hit real architectural decisions, real packaging challenges, and real Flathub review feedback.</p>
<p>Every post will be self-contained enough that you can follow along building your own app. The problems we&rsquo;ll solve — state management, async operations, data persistence, adaptive layouts, packaging — are universal to any non-trivial GTK application. We&rsquo;re documenting the actual experience of taking a GNOME app from zero to Circle, including the parts that aren&rsquo;t in any documentation.</p>
<hr>
<h2 id="what-comes-next">What comes next</h2>
<p>Before we build a window, we need to understand the type system underneath it. Every widget, every signal, every property binding in GTK is built on GObject — and the way GObject maps to Rust is the single biggest conceptual hurdle for developers coming from other Rust backgrounds.</p>
<p>In Part 2, we&rsquo;ll build a simple data model from scratch and use it to understand GObject&rsquo;s inner/outer type pattern, properties, signals, and property bindings. No GTK widgets yet — just the foundation that makes everything else click once we start building the actual app in Part 3.</p>
]]></content:encoded></item></channel></rss>