Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome to this tutorial...

This guide explains how to build a complete Weather Application using modern Linux development tools:

  • Rust
  • GTK4
  • Libadwaita
  • Flatpak

The goal of this tutorial is to help new developers understand how modern Linux applications are structured, built, and distributed.

Overview

In this tutorial, you will create a fully functional graphical application that:

  • Displays current weather information
  • Shows a 7-day forecast
  • Allows users to search for cities
  • Saves selected cities locally
  • Uses a responsive user interface
  • Fetches data from a public web API
  • Runs safely using background threads
  • Is packaged and distributed using Flatpak

The application will follow current best practices for GTK4 and Libadwaita development.

Purpose of This Guide

Developing applications for Linux can initially feel complex. Developers often need to understand:

  • System dependencies
  • GUI frameworks
  • Runtime environments
  • Sandboxed packaging systems
  • Version compatibility

This guide provides a structured path through these topics.

It begins with a completely empty system and builds the application step by step. Each concept is introduced when needed and explained in context.

The intention is not only to provide working code, but to explain:

  • Why each component is necessary
  • How the different tools interact
  • What common errors mean
  • How to debug problems effectively

Structure of This Tutorial

This documentation is divided into logical sections:

  1. Environment Setup
  2. Creating the Project
  3. Building the User Interface
  4. Adding Application Logic
  5. Fetching and Displaying Data
  6. Threading and Concurrency
  7. Data Persistence
  8. Debugging and Common Errors
  9. Flatpak Packaging

Each section builds upon the previous one.

You are encouraged to follow the chapters in order.

Audience

This tutorial is designed for developers who want to learn how modern Linux applications are built using Rust, GTK4, Libadwaita, and Flatpak.

It is especially suitable for:

  • Students learning Linux application development
  • Developers who know basic Rust and want to build graphical applications
  • Developers new to GTK4 and Libadwaita
  • Developers interested in Flatpak packaging and sandboxing
  • Anyone curious about how desktop and mobile-ready Linux apps are structured

Required Knowledge

To follow this tutorial comfortably, you should have:

  • Basic understanding of Rust syntax
    (variables, functions, structs, simple error handling)
  • Basic familiarity with the terminal
    (running commands, navigating directories)
  • A willingness to experiment and debug

We will explain all GTK, Flatpak, and architecture concepts from the beginning.

No Prior Experience Required

You do not need:

  • Previous GTK experience
  • Knowledge of Libadwaita
  • Mobile development experience
  • Experience with Flatpak
  • Knowledge of Linux packaging systems

All required tools and concepts will be introduced step by step.

What Kind of Learner Is This For?

This guide is ideal if you:

  • Prefer structured, step-by-step instructions
  • Want to understand why something works not just copy code
  • Are comfortable reading documentation
  • Want to build a real application instead of a minimal example

The tutorial focuses on clarity and gradual learning.

Development Environment Assumptions

This tutorial assumes you are working on:

  • A Linux system (Debian or similar)
  • A clean or newly prepared development environment
  • A system where you can install packages and run development tools

All setup steps are explained in the Environment Setup section.

How to Use This Tutorial

This sections explains how to work through this guide effectively. While you can read individual sections independently, this tutorial is designed to be followed in order.

The tutorial is structured progressively:

  1. Environment Setup
  2. Project Creation
  3. User Interface Design
  4. Application Logic
  5. API Integration
  6. Threading and Concurrency
  7. Local Data Storage
  8. Debugging
  9. Flatpak Packaging

Each section builds on knowledge from the previous one.

For best results, follow the chapters sequentially.

Estimated Time Commitment

This tutorial is designed to be completed in approximately:

  • 8–10 hours total
    or
  • 2–3 focused sessions

Time may vary depending on:

  • Your familiarity with Rust
  • Your Linux experience
  • Whether you explore additional experiments

Do not rush. The goal is understanding, not speed.

Development Setup Recommendation

It is strongly recommended to use:

  • A clean Linux installation
    or
  • A fresh virtual machine (VM)

Why?

Because this tutorial simulates the onboarding experience of a new developer starting from scratch.

Using a clean system ensures:

  • You install all required dependencies yourself
  • You see common setup issues
  • You understand how the full environment works

If you already have Rust or GTK installed, that is fine but be aware that some steps may behave differently.

How to Follow Along

For each chapter:

  1. Read the explanation carefully.
  2. Type the commands manually (avoid copy-pasting everything).
  3. Run the application after each major step.
  4. Verify that your output matches the expected result.
  5. Only then continue to the next section.

Typing commands manually helps you understand what is happening.

Handling Errors

It is normal to encounter errors during development.

Common examples include:

  • Missing system libraries
  • Version mismatches
  • Flatpak runtime issues
  • GTK builder validation errors
  • Network-related API errors

If something fails:

  1. Read the error message carefully.
  2. Compare your output with the documentation.
  3. Check the "Debugging and Common Errors" section.
  4. Search the official documentation if necessary.

Do not panic debugging is part of the learning process.

Experimentation Is Encouraged

You are encouraged to experiment during this tutorial.

For example:

  • Change window sizes and observe layout behavior
  • Modify labels and UI elements
  • Try invalid city names
  • Remove network permission in Flatpak and observe what happens
  • Intentionally break something and fix it

Understanding how systems fail is just as important as understanding how they work.

Understanding the Structure

Each chapter typically includes:

  • Concept explanation
  • Code example
  • Step-by-step instructions
  • Expected output
  • Notes about common pitfalls

Try to understand:

  • Why each dependency is needed
  • Why background threads are required
  • Why Flatpak requires specific permissions
  • Why certain GTK widgets are chosen

The goal is comprehension, not just completion.

When You Get Stuck

If you are stuck:

  • Re-read the previous section
  • Check that all dependencies are installed
  • Verify your Cargo.toml versions
  • Rebuild using cargo clean
  • Rebuild Flatpak using --force-clean

Sometimes rebuilding from scratch solves environment-related issues.

Final Advice Before You Begin

  • Be patient.
  • Take notes.
  • Test frequently.
  • Do not skip sections.

By the end of this tutorial, you will not only have a working application you will understand how modern Linux applications are built, structured, and distributed.

Continue to the next chapter to prepare your development environment.

VM Setup

Before we start installing Rust, GTK, or Flatpak, we need to prepare a clean development environment.

This tutorial assumes you are starting from a fresh Linux system.

The recommended approach is to use a Virtual Machine (VM).

What Is a Virtual Machine?

A Virtual Machine (VM) is a software-based computer that runs inside your real computer.

It allows you to:

  • Install a fresh operating system
  • Test development environments safely
  • Avoid breaking your main system
  • Reproduce a clean "first-time developer" setup

Think of it as a separate computer running inside your laptop.

Why Use a Virtual Machine?

Using a VM ensures that:

  • No tools are preinstalled
  • No hidden dependencies exist
  • The setup process reflects a real onboarding experience
  • All required packages are installed manually
  • Errors are visible and reproducible

For learning and evaluation purposes, this is extremely important.

If you already have Rust and GTK installed globally, you may skip some setup steps but using a VM is strongly recommended.

You can use any virtualization software such as:

  • VirtualBox
  • GNOME Boxes
  • VMware

For this tutorial, the recommended configuration is:

  • Operating System: Debian 13
  • RAM: 4 GB (minimum 2 GB)
  • Disk Space: 20 GB
  • CPU: 2 cores (recommended)

These values provide enough resources to build Rust projects and Flatpak packages comfortably.

Step 1 - Install a Linux Distribution

Download one platform :

  • Debian (stable)

Install the operating system inside your VM.

During installation:

  • Enable standard utilities
  • Enable network access
  • Create a user account with sudo privileges

Once installation is complete, log in to your new system.

Step 2 - Update the System

Before installing development tools, update your system:

sudo apt update
sudo apt upgrade -y

Step 3 - Install Basic Development Tools

Install essential build tools:

sudo apt install build-essential curl git pkg-config -y

Step 4 - Verify Network Access

Since this tutorial fetches data from an online API, verify that your system has internet access:

ping google.com

If this works, your network is configured correctly.

Optional - Install a Code Editor

You may use any editor, such as:

  • VS Code
  • GNOME Builder
  • Vim
  • Nano

For beginners, VS Code is often the most comfortable choice.

Example installation:

sudo snap install code --classic

install from your distribution’s package manager.

Install Rust

Rust is the programming language we will use to build our Weather Application.

  • Install Rust using the official installer
  • Verify that the installation works
  • Understand what Cargo is
  • Confirm your development environment is ready

What Is Rust?

Rust is a modern systems programming language focused on:

  • Performance
  • Memory safety
  • Concurrency
  • Reliability

Rust is widely used for:

  • Command-line tools
  • Backend services
  • WebAssembly
  • Desktop applications (including GTK apps)

We will use Rust together with GTK4 and Libadwaita to build a graphical application.

Step 1 - Install Rust Using rustup

Rust is installed using rustup, the official Rust toolchain installer.

Run the following command:

curl https://sh.rustup.rs -sSf | sh

You will see a message explaining installation options.

When prompted, select:

1) Proceed with installation (default)

Press Enter.

The installer will:

  • Download the Rust compiler
  • Install Cargo (Rust’s package manager)
  • Configure your environment

This may take a few minutes.

Step 2 - Configure Your Environment

After installation completes, you may see a message like:

To get started you may need to restart your current shell.

Run:

source $HOME/.cargo/env

This ensures the Rust tools are available in your terminal session.

Step 3 - Verify Installation

Check that Rust and Cargo is installed correctly:

rustc --version
cargo --version

If both commands work, Rust is installed successfully.

What Is Cargo?

Cargo is Rust’s:

  • Build system
  • Package manager
  • Dependency manager
  • Project generator

You will use Cargo to:

  • Create new projects
  • Add dependencies
  • Compile your application
  • Run your application

Think of Cargo as the central tool for Rust development.

To confirm everything works, create a simple test project:

cargo new rust_test
cd rust_test
cargo run

You should see:

Hello, world!
cd ..
rm -rf rust_test

Common Issues and Solutions

Command Not Found: rustc or cargo

If you see:

command not found: rustc

Run:

source $HOME/.cargo/env

If the issue persists, restart your terminal.

curl Not Found

If curl is missing, install it:

sudo apt install curl -y

Permission Errors

Do not use sudo to install Rust with rustup.

Rust should be installed as a normal user.

Why We Use rustup

Rustup allows:

  • Managing multiple Rust versions
  • Updating Rust easily
  • Installing additional components
  • Keeping your system organized

To update Rust later, simply run:

rustup update

for more details go through the Rust Documentation

Install GTK4 and Libadwaita

Now that Rust is installed, we need to install the graphical libraries required to build GTK applications.

Rust alone is not enough to build a graphical interface. GTK applications depend on system libraries that must be installed separately.

In this chapter, you will:

  • Install GTK4 development libraries
  • Install Libadwaita
  • Install required system tools
  • Verify that the setup works

What Are GTK4 and Libadwaita?

GTK4

GTK (GIMP Toolkit) is a cross-platform library for building graphical user interfaces.

GTK4 is the modern version used for:

  • Linux desktop applications
  • GNOME applications
  • Mobile-adaptive applications

It provides widgets such as:

  • Windows
  • Buttons
  • Labels
  • List views
  • Layout containers

Libadwaita

Libadwaita is a companion library built on top of GTK4.

It provides:

  • Modern GNOME design components
  • Responsive layout tools
  • Adaptive UI behavior
  • Navigation patterns for desktop and mobile

In this tutorial, we will use Libadwaita to create a modern, responsive interface.

Why System Libraries Are Required

When you add this dependency in Cargo.toml:

gtk4 = "0.9"

Rust does not download GTK itself.

Instead:

  • The Rust crate connects to system-installed GTK libraries.
  • The system must already provide GTK development headers.

If GTK is not installed, you will see build errors like:

pkg-config not found
gtk4 not found

That is why this step is required.

Step 1 - Install GTK4 Development Packages

Run:

sudo apt install libgtk-4-dev -y

This installs:

  • GTK4 runtime libraries
  • Development headers
  • Required system components

Step 2 - Install Libadwaita Development Packages

Run:

sudo apt install libadwaita-1-dev -y

This installs:

  • Libadwaita runtime
  • Development headers

Step 3 - Install Additional Required Tools

Install pkg-config:

sudo apt install pkg-config -y

Install GLib development files:

sudo apt install libglib2.0-dev -y

These tools allow Rust to detect installed system libraries during compilation.

Step 4 - Verify Installation

You can verify GTK4 installation using:

pkg-config --modversion gtk4

You should see a version number such as:

Now verify Libadwaita:

pkg-config --modversion libadwaita-1

If both commands return version numbers, the installation was successful.

What If pkg-config Fails?

If you see:

Package gtk4 was not found

Make sure:

  • libgtk-4-dev is installed
  • You ran sudo apt update
  • Your distribution version supports GTK4

If you are using an older Linux distribution, GTK4 may not be available in default repositories.

Why This Step Is Important

GTK and Libadwaita are native libraries written in C.

Rust bindings rely on these native system components.

Without them:

  • Your project will not compile
  • Cargo will fail during build
  • Flatpak builds may also fail

Installing these correctly ensures:

  • Cargo can detect GTK
  • Your application builds successfully
  • Your development environment is complete

For more details can be found in Gtk and Libadwaita Documentation

Install Flatpak

In this chapter, you will install Flatpak and prepare the required runtime environment for building and running the application inside a sandbox.

Flatpak is an essential part of modern Linux application distribution.

What Is Flatpak?

Flatpak is a system for:

  • Building applications
  • Distributing applications
  • Running applications in a sandbox

Unlike traditional package managers, Flatpak:

  • Bundles application dependencies
  • Runs looking at a defined runtime
  • Is isolated from the host system
  • Works across different Linux distributions

Flatpak ensures that your application runs consistently across systems.

Why We Use Flatpak in This Tutorial

In modern GNOME and LinuxMobile development:

  • Applications are expected to be sandboxed
  • Distribution often happens through Flathub
  • Runtimes are standardized (e.g., GNOME Platform)

Using Flatpak allows you to:

  • Reproduce builds reliably
  • Avoid host system conflicts
  • Test your application in a controlled environment

Step 1 - Install Flatpak

Run:

sudo apt install flatpak -y

Now verify installation:

flatpak --version

Step 2 - Add Flathub Repository

Flathub is the main Flatpak application repository.

Add it with:

flatpak remote-add --if-not-exists flathub \
https://flathub.org/repo/flathub.flatpakrepo

This allows your system to download runtimes and SDKs.

Step 3 - Install GNOME Runtime

Our project uses:

  • GNOME Platform
  • GNOME SDK
  • Version 49

Install the runtime:

flatpak install flathub org.gnome.Platform//49

Install the SDK:

flatpak install flathub org.gnome.Sdk//49

The runtime provides:

  • GTK libraries
  • System dependencies
  • Shared components

The SDK provides:

  • Development tools
  • Build utilities
  • Compilation support

Step 4 - Install Rust SDK Extension

Since we are building a Rust application, we must install the Rust extension for the GNOME SDK.

Run:

flatpak install flathub org.freedesktop.Sdk.Extension.rust-stable

After running this command, Flatpak will show a list of available versions.

It will look similar to this:

Looking for matches...

1) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/21.08
2) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/1.6
3) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/22.08
4) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/23.08
5) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/18.08
6) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/24.08
7) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/19.08
8) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/25.08
9) runtime/org.freedesktop.Sdk.Extension.rust-stable/x86_64/20.08

Flatpak will then ask:

Which do you want to use (0 to abort)? [0-9]:

What should you type?

Type:

8

and press Enter.

The number 8 selects the latest stable Rust SDK version (25.08). Using the newest version ensures compatibility with the latest GNOME SDK and Rust tooling.

After selecting the number, Flatpak will start downloading the Rust toolchain.

You will see something like:

Installing...
22% 1.5 MB/s

Wait until the installation finishes.

This extension provides:

  • Rust compiler inside the Flatpak build environment
  • Cargo support
  • Required toolchain integration

Without this extension, Flatpak builds will fail.

Step 5 - Verify Installed Runtimes

List installed runtimes:

flatpak list

You should see entries for:

  • org.gnome.Platform 49
  • org.gnome.Sdk 49
  • org.freedesktop.Sdk.Extension.rust-stable

If they appear, your Flatpak environment is ready.

Understanding Runtime vs SDK

Runtime

The runtime is what your application runs on.

It contains:

  • GTK
  • System libraries
  • Shared components

It ensures consistency across different Linux systems.

SDK

The SDK is used during build time.

It contains:

  • Compilers
  • Debug tools
  • Headers
  • Build environment tools

You build against the SDK. Users run against the runtime.

Why Flatpak Matters for Onboarding

Flatpak introduces additional complexity:

  • Large runtime downloads
  • Version alignment requirements
  • Sandbox permissions
  • Manifest configuration

Understanding these early helps you:

  • Debug build errors
  • Understand permission requirements
  • Package applications correctly

Flatpak is not just a packaging tool it defines the application environment.

Common Issues and Solutions

Runtime Version Not Found

If you see:

Requested runtime not installed

Make sure you specify the correct version:

org.gnome.Platform//49

Check available versions:

flatpak search org.gnome.Platform

Rust Extension Version Mismatch

If you see:

Requested extension not installed

Install the rust-stable extension without specifying version:

flatpak install flathub org.freedesktop.Sdk.Extension.rust-stable

Disk Space Issues

Flatpak runtimes can consume several gigabytes.

If space is limited:

flatpak uninstall --unused

for more details go through the Flatpak Documentation

Understanding the Weather Application Structure

When building a GTK4 and Libadwaita application, we are not only writing code. We are designing a graphical interface made of different UI components called widgets.

Widgets are the visual elements that users interact with. Examples include buttons, text labels, lists, and layout containers.

In this Weather Application, every visible part of the interface is built using widgets from GTK4 and Libadwaita.

GTK4 provides the basic building blocks such as GTK Box, GTK Button, GTK Label, GTK ListBox, GTK Stack, and GTK Popover.

Libadwaita provides modern GNOME interface components such as HeaderBar, NavigationSplitView, StatusPage, ActionRow, MessageDialog, and Breakpoint.

Before learning each widget individually, it is helpful to first look at how the whole application is structured.

What the App Looks Like When It Starts

When the Weather Application runs for the first time, there are no cities saved yet. Because of this, the interface shows an empty state.

Figure 1: Initial state of the Weather Application showing empty sidebar

Figure 1 shows the application when it first opens.

At the top of the window, we can see the HeaderBar. The HeaderBar replaces the traditional menu bar and gives the application a modern GNOME design.

Figure 6: Application header bar

Figure 6: HeaderBar showing the application title and navigation controls.

Inside the header bar, a GTK Label displays the title of the application, and GTK Button widgets provide navigation controls.

Below the header bar, the layout is divided using NavigationSplitView. This widget creates two areas in the application: a left sidebar and a right content area.

Inside the sidebar, we use a vertical GTK Box to arrange elements from top to bottom.

At the top of this box, there is a GTK SearchEntry which allows users to search for cities. Next to it, there is a GTK Button with a plus icon used to add a new city.

Since no city has been added yet, the sidebar displays a StatusPage.

The StatusPage is a Libadwaita widget used to show helpful messages when there is no data to display. Instead of showing an empty area, it guides the user with a message such as “Add your first city.”

This design makes the application feel complete even when it contains no data yet.

What the App Looks Like After Adding a City

After a city is added and selected, the interface changes and shows the weather details.

Figure 2: Weather view displaying selected city and 7-day forecast

Figure 2 shows the application after a city has been added.

The NavigationSplitView now clearly divides the app into two working areas.

The left side displays the saved cities list.

The right side shows the weather information for the selected city.

The list of cities is displayed using GTK ListBox. Each city is represented using an ActionRow.

ActionRow is a Libadwaita widget designed for list items that contain structured information. Each row shows the city name and may include additional elements such as icons or buttons.

In this app, each row also includes a GTK Button with a trash icon that allows the user to delete the city.

On the right side of the window, the weather details are displayed.

This part of the interface is controlled by a GTK Stack.

A GTK Stack allows multiple views to exist in the same area but shows only one at a time.

When no city is selected, the stack shows an empty view. When a city is selected, it switches to the weather information view.

Inside the weather view, different widgets are used.

A GTK Image displays the weather icon. GTK Label widgets display the city name and the current temperature.

Below this information, another GTK ListBox displays the 7-day weather forecast.

Each forecast entry is again displayed using an ActionRow.

This shows how widgets such as ListBox and ActionRow can be reused for different purposes within the same application.

The Main Controller of the Application

Every GTK application starts with an Application. In Libadwaita applications, we use AdwApplication.

The Application is not visible to the user. Instead, it controls how the program starts and how windows are managed.

It connects the application to the operating system and ensures that everything runs correctly.

You can think of the Application as the manager of the program. It starts the application, keeps it running, and manages its windows.

Without an Application, the program would not be able to create any windows.

The Main Window of the Application

The first visible part of the program is the ApplicationWindow.

The ApplicationWindow acts as the main container for the entire interface. It defines the size of the application and holds all other widgets.

At the top of the window, we place a HeaderBar.

The HeaderBar replaces the traditional menu bar and provides a modern GNOME-style top area.

Inside the header bar, we use:

  • GTK Button widgets for navigation actions
  • A GTK Label to display the application title “Weather App”

A GTK Button is an interactive widget that performs an action when the user clicks it.

A GTK Label simply displays text on the screen.

Showing Messages to the User

Applications must guide users when something goes wrong.

In this Weather App, we use MessageDialog to show feedback messages.

MessageDialog is a Libadwaita dialog window that appears on top of the main application window.

For example, if the user tries to add a city without typing a name, the application shows a message explaining that the city name cannot be empty.

If the user tries to add a city that already exists, another dialog informs them that the city is already saved.

Add City Dialog

Figure: Dialog displayed when the user clicks the "+" button.

Dialogs temporarily block interaction with the main window until the user closes them. This ensures that important messages are noticed.

Dividing the Interface into Two Parts

To divide the window into a sidebar and a content area, the application uses NavigationSplitView.

NavigationSplitView is a Libadwaita widget that automatically creates a two-pane layout.

In this Weather App:

The left pane contains the city list and search tools.

The right pane displays the weather information.

On large screens, both panes are visible at the same time.

On smaller screens, the layout collapses so that only one pane is visible at a time.

This makes the application responsive and usable on different screen sizes.

Organizing the Sidebar Layout

Inside the sidebar, widgets are arranged using GTK Box.

A GTK Box is a layout container that arranges widgets either vertically or horizontally.

In the Weather App sidebar, we use a vertical box to place widgets one below another.

At the top of the sidebar, there is a horizontal box containing two elements:

A GTK SearchEntry, which allows users to type and search for cities.

A GTK Button with a plus icon, which opens the dialog for adding a new city.

Using boxes helps control spacing and alignment so that the interface looks clean and organized.

Handling the Empty Sidebar

When the application starts and no cities are saved, the sidebar would normally appear empty.

To improve the user experience, we use a StatusPage.

StatusPage is a Libadwaita widget designed to display helpful placeholder messages.

In this application, it shows a message encouraging the user to add their first city.

Empty sidebar state showing StatusPage

Figure: The application sidebar when no cities have been added yet.

This approach prevents the interface from looking incomplete.

Displaying the Saved Cities

The saved cities are displayed using GTK ListBox.

GTK ListBox is a container that organizes rows vertically.

Each city is displayed using an ActionRow.

Saved city displayed in the sidebar

Figure: The sidebar displaying a saved city using a GTK ListBox and ActionRow.

An ActionRow usually contains:

A title (the city name)

Optional subtitle text

Optional icons or buttons

In this Weather App, each row also includes a delete button with a trash icon.

Because ActionRow follows GNOME design guidelines, it helps the application maintain a consistent and modern appearance.

Displaying Weather Information

The weather information is displayed in the right pane.

This area is controlled by a GTK Stack.

A GTK Stack allows multiple views to exist in the same area but only one is visible at a time.

The stack switches between:

An empty state view

The weather information view

Inside the weather view, several widgets are used.

GTK Label widgets display the city name and temperature.

A GTK Image shows the weather icon.

Below this information, a GTK ListBox displays the 7-day forecast.

Each forecast entry is displayed using an ActionRow.

Making the Interface Interactive

Widgets become interactive through signals.

Signals are events that occur when a user interacts with a widget.

For example, when a user clicks a GTK Button, the button emits a clicked signal.

The application then runs a function called a callback to respond to that event.

Examples in this application include:

Clicking the plus button opens the add city dialog.

Selecting a city row displays the weather information.

Navigation arrow showing transition to weather view

Figure: Navigation arrow allowing the user to move between the sidebar and the weather view.

Typing in the search field shows city suggestions.

Signals connect user actions with application behavior.

Showing City Suggestions

When the user types in the search field, the application displays city suggestions.

This is implemented using GTK Popover.

A GTK Popover is a floating container that appears near another widget.

In this case, the popover appears below the SearchEntry.

Inside the popover, we place a GTK ListBox containing suggestion rows.

This creates a dropdown-style list of possible cities.

Adapting the App to Different Screen Sizes

Modern applications must work on both large and small screens.

Libadwaita provides Breakpoint to support responsive design.

A breakpoint defines how the layout should change when the window becomes smaller.

In this Weather App:

When the window becomes narrow, the NavigationSplitView collapses.

Only one pane is visible at a time.

Navigation buttons allow the user to move between the sidebar and the weather view.

This ensures that the application works well on desktops, tablets, and small windows.

Understanding Cargo and Project Setup

In the previous chapter, we looked at the structure of the Weather Application and the widgets used to build the user interface.

Now we will learn about Cargo, the tool that manages Rust projects.

Cargo helps us build, run, and organize our application. It also manages external libraries that our project depends on.

Understanding Cargo will help you see how Rust code, GTK libraries, and external packages work together to create a complete application.

What Is Cargo?

Cargo is the official build tool and package manager for Rust.

It performs several important tasks for developers.

Cargo can:

  • Create new Rust projects
  • Download external libraries (called crates)
  • Compile the program
  • Run the application
  • Manage project versions and dependencies

Instead of manually compiling many files, Cargo handles everything automatically.

When we run a command such as:

cargo run

Cargo first compiles the project and then runs the application.

Because of this, Cargo becomes the central tool used in almost every Rust project.

The Cargo.toml File

Every Rust project contains a configuration file called Cargo.toml.

This file tells Cargo important information about the project, such as:

  • The project name
  • The version of the application
  • The Rust edition used
  • The external libraries required by the project

Open the Cargo.toml file in the Weather App project. You will see something similar to this:

[package]
name = "weatherapp"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
gtk4 = "0.9"
libadwaita = { version = "0.7", features = ["v1_4"] }

reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
urlencoding = "2.1"

[build-dependencies]
glib-build-tools = "0.18"

This file defines how the project is built and which libraries are used.

Project Information

The [package] section contains basic information about the project.

[package]
name = "weatherapp"
version = "0.1.0"
edition = "2021"

The name defines the name of the application. Cargo will create an executable file with this name.

The version shows the current version of the project. Version numbers become important when distributing applications.

The edition defines the Rust language version used by the project. Using the 2021 edition enables modern Rust features.

Project Dependencies

The [dependencies] section lists external libraries used by the application.

Rust libraries are called crates.

When you build the project, Cargo automatically downloads these crates from the Rust package registry.

For example, the Weather App uses several important crates.

GTK4

gtk4 = "0.9"

This crate provides Rust bindings for the GTK4 graphical user interface toolkit.

It allows us to create windows, buttons, labels, and other interface elements.

Libadwaita

libadwaita = { version = "0.7", features = ["v1_4"] }

Libadwaita provides modern GNOME interface components.

Widgets such as:

  • HeaderBar
  • NavigationSplitView
  • StatusPage
  • ActionRow

come from this library.

These widgets help create applications that follow GNOME design guidelines.

reqwest

reqwest = { version = "0.11", features = ["blocking", "json"] }

The reqwest crate is used to send HTTP requests.

In this Weather App, it is used to fetch weather data from the Open-Meteo API.

The enabled features allow the program to:

  • Send requests easily (blocking)
  • Automatically handle JSON responses (json)

serde

serde = { version = "1.0", features = ["derive"] }

The serde crate is used to convert JSON data into Rust data structures.

Weather APIs usually return data in JSON format. Serde allows the application to easily convert this data into Rust variables.

urlencoding

urlencoding = "2.1"

This crate ensures that city names are correctly formatted when used in web requests.

For example:

New York → New%20York

This encoding prevents errors when sending requests to the weather API.

Build Dependencies

Some tools are only needed during compilation.

These are defined in the [build-dependencies] section.

[build-dependencies]
glib-build-tools = "0.18"

In the Weather App, this tool is used to compile GTK resource files such as .ui interface files.

This process is triggered by a special script called build.rs.

How Cargo Builds the Application

When you run the following command:

cargo build

Cargo performs several steps automatically.

First, it checks the dependencies listed in Cargo.toml. Then it downloads the required crates from the Rust package registry.

After that, it compiles all crates and links them together with the project code.

Finally, Cargo produces the application executable.

The compiled files are placed in the target directory.

Running the Application

To build and run the Weather App, you can use the following command:

cargo run

Cargo will compile the project and start the application.This command is very convenient during development because it performs both steps automatically.

open the real file in the project

You can also open the file directly from the project folder:

weather-app/Cargo.toml

Creating the First Window

In this section, we will create the first graphical window of the Weather Application.

Before building the full application with a sidebar, weather panel, and API integration, we first need to confirm that GTK4 and Libadwaita are working correctly.

The goal of this step is simple: launch the application and display a basic window.

Project Structure

Your project currently looks like this:

weatherapp/
├── Cargo.toml
├── build.rs
└── src/
    └── main.rs

The main.rs file is the starting point of the program. This is where the GTK application begins.

The file build.rs is a special Rust build script. It is executed by Cargo before compiling the application.

The build.rs File

At this stage, you may notice that the project already contains a file called build.rs.

This file is not used to create the application window itself. Instead, it is used during the build process to prepare additional project resources.

In GTK applications, build.rs is often used later to compile files such as:

  • UI definition files
  • resource XML files
  • icons
  • other bundled application data

For now, you do not need to change anything inside build.rs to display the first window. We include it in the project structure because it will become important in later chapters when we work with GTK resources.

A simple example of build.rs looks like this:

fn main() {
    glib_build_tools::compile_resources(
        &["resources"],
        "resources/resources.xml",
        "resources.gresource",
    );
}

This tells Rust to compile the resource files from the resources/ folder before building the application.

If your project already contains this file, you can leave it as it is. If you do not yet have a resources folder or resource files, that is fine they will be introduced later when needed.

Basic GTK + Libadwaita Window

Replace the contents of src/main.rs with the following code.

use gtk4 as gtk;
use libadwaita as adw;

use gtk::prelude::*;

const APP_ID: &str = "com.example.WeatherApp";

fn main() {
    adw::init();

    let app = adw::Application::builder()
        .application_id(APP_ID)
        .build();

    app.connect_activate(build_ui);
    app.run();
}

fn build_ui(app: &adw::Application) {
    let title_label = gtk::Label::new(Some("Weather App"));

    let header = adw::HeaderBar::builder()
        .title_widget(&title_label)
        .build();

    let empty_content = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();

    let main_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(0)
        .build();

    main_box.append(&header);
    main_box.append(&empty_content);

    let window = adw::ApplicationWindow::builder()
        .application(app)
        .title("Weather App")
        .default_width(900)
        .default_height(750)
        .content(&main_box)
        .build();

    window.present();
}

What This Code Does

This code creates the simplest possible GTK + Libadwaita application window.

The program starts by initializing Libadwaita. This ensures that the application uses the correct GNOME styling and theme behavior.

Next, an Application object is created. This object manages the lifecycle of the entire program, including startup, window creation, and closing the application.

When the application starts, GTK triggers the activation event. At that moment, the function build_ui() is called to create the user interface.

Inside this function, we create two main elements.

The first element is a HeaderBar. The header bar is the top section of the application window and contains the title “Weather App.”

The second element is the ApplicationWindow. This is the main window of the program.

The window is configured with a default size of 900 × 750 pixels, which provides enough space for the future layout of the Weather App.

Finally, the window is presented so that it becomes visible to the user.

Running the Application

Open a terminal inside the project directory and run:

cargo run

Cargo will compile the project and launch the application.

Expected Result

If everything is configured correctly, you should see:

  • A window titled Weather App
  • A header bar at the top
  • An empty content area

This confirms that GTK4 and Libadwaita are working correctly in your system.

In the next chapters, we will begin building the real interface by adding:

  • The sidebar
  • The city search feature
  • The weather display panel

View the Complete Weather App Code

If you want to see the full working implementation of the Weather Application, you can open the original source code of the project.

You can view the main program file here:

Weather App Main File src/main.rs

You can also view the build script here:

Build Script File build.rs

This file is responsible for compiling bundled GTK resources during the build process.

By opening these files, you can explore the full implementation of the application and understand how all components work together.

Two important improvements from the feedback:

  1. Do not suddenly show build.rs code under “View the Complete Weather App Code” without first explaining what build.rs is.
  2. Add one sentence like this:

You do not need to fully understand build.rs yet; it is included here so you know where it comes from, and we will use it later when adding resources.

Layout Overview Designing the Application Structure

Before implementing logic, networking, or storage, a developer must define the user interface architecture.

This chapter explains how the Weather App layout is structured and why specific UI design decisions were made.

The focus here is not code syntax, but layout strategy and UI reasoning.

1. Purpose of the Layout

The Weather App must support:

  • Desktop usage (wide screens)
  • Tablet usage
  • Linux mobile environments

Therefore, the UI must:

  • Be responsive
  • Separate navigation from content
  • Avoid duplicate layouts for mobile and desktop

To achieve this, the application uses a structured layout hierarchy.

2. High-Level UI Architecture

The complete UI structure follows this hierarchy:

ApplicationWindow
    └── Vertical Layout
            ├── HeaderBar
            └── NavigationSplitView
                    ├── Sidebar (Cities)
                    └── Content Panel (Weather)

Each layer has a specific responsibility.

3. ApplicationWindow

The ApplicationWindow is the root container.

Responsibilities:

  • Hosts the entire UI
  • Manages window size
  • Integrates with the GNOME desktop environment
  • Handles breakpoints for responsiveness

This window is intentionally created with:

  • A reasonable default desktop size (900x750)
  • Flexibility for resizing

4. Vertical Layout Container

Inside the window, a vertical container is used.

Why vertical?

Because the interface is divided into:

  1. A fixed top area (HeaderBar)
  2. A dynamic content area (Split View)

This separation ensures that:

  • The header remains stable
  • Only the content area changes during navigation

5. HeaderBar (Top Navigation Layer)

The HeaderBar provides:

  • Application title
  • Navigation arrows (in mobile mode)
  • Future action buttons (Add, Search, etc.)

Design reasoning:

  • Modern GNOME applications use HeaderBar instead of traditional title bars
  • It allows embedding actions directly in the top bar
  • It adapts automatically to system themes

The HeaderBar does not scroll and remains constant.

6. NavigationSplitView - Core Layout Engine

The most important UI component in the layout is NavigationSplitView.

It allows:

  • Two-panel layout on large screens
  • Collapsible single-page layout on small screens

Desktop Mode

[ Sidebar | Weather Content ]

Desktop layout showing sidebar and weather panel

Figure: Desktop layout where the sidebar and weather content are visible at the same time.

Mobile Mode

[ Sidebar ]
   ↓ (select city)
[ Weather Content ]

Mobile layout showing navigation between sidebar and weather view

Figure: Mobile layout where only one panel is visible and navigation arrows allow switching between pages.

This eliminates the need to create separate UI files for desktop and mobile.

7. Sidebar (Navigation Panel)

The sidebar is responsible for:

  • Displaying saved cities
  • Providing search functionality
  • Showing an empty-state message

Structurally, it contains:

  • A search row (SearchEntry + Add button)
  • A StatusPage (when empty)
  • A scrollable list of cities

The sidebar acts as the navigation layer of the application.

It does not display weather data.

8. Content Panel (Information Layer)

The content panel displays:

  • Selected city name
  • Current temperature
  • 7-day forecast list

It is implemented using a stack with two states:

  • Empty state (no city selected)
  • Weather display state

Why use a stack?

Because:

  • At startup, no city is selected
  • The UI should not display invalid or placeholder data
  • State switching becomes clean and controlled

9. Responsive Design Strategy

Responsiveness is handled using breakpoints.

If the window width is small:

  • The split view collapses
  • Only one panel is visible at a time
  • Navigation arrows become visible

If the window width is large:

  • Both panels are visible simultaneously
  • Navigation arrows are hidden

This design ensures:

  • Desktop usability
  • Mobile compatibility
  • Consistent user experience

10. UX Design Considerations

Several user experience decisions were made:

  • No weather shown before city selection
  • Forward navigation disabled if no cities exist
  • Clear empty-state instructions
  • No duplicate "Add" buttons
  • Navigation arrows only when necessary

These decisions prevent:

  • User confusion
  • Dead-end navigation
  • Empty screens without explanation

11. Architectural Benefits

This layout design provides:

  • Clear separation of concerns
  • Logical UI grouping
  • Expandability for future features
  • Compatibility with Linux Mobile environments
  • Maintainable structure for onboarding developers

It demonstrates modern GNOME application architecture using Libadwaita.

NavigationSplitView Internal Behavior and Adaptive Navigation Model

The NavigationSplitView is the architectural core of the Weather App’s interface. This is not just a layout widget it defines how users move through the application.

1. Conceptual Model

At a high level, NavigationSplitView manages two regions:

NavigationSplitView
    ├── Sidebar (Navigation Layer)
    └── Content (Detail Layer)

These are not simple containers. They are wrapped in NavigationPage objects to enable:

  • Title handling
  • Navigation transitions
  • Stack-like behavior in mobile mode

Minimal structural setup looks like:

#![allow(unused)]
fn main() {
let split = NavigationSplitView::new();
split.set_sidebar(Some(&sidebar_page));
split.set_content(Some(&content_page));
}

This defines the two primary UI regions.

2. Expanded Mode (Desktop Behavior)

When the window is wide enough:

#![allow(unused)]
fn main() {
collapsed = false
}

Behavior:

  • Sidebar and content are displayed simultaneously.
  • Both panels share horizontal space.
  • Navigation transitions are disabled.
  • Back/forward arrows are hidden.

Visual layout:

[ Sidebar | Content ]

Desktop split layout showing sidebar and content panel

Figure: Desktop mode where sidebar and weather content are visible simultaneously.

Internally:

  • Both pages are active.
  • The layout engine distributes width proportionally.
  • show_content property becomes irrelevant.

This mode optimizes:

  • Efficiency
  • Visibility
  • Desktop multitasking

3. Collapsed Mode (Mobile Behavior)

When the window becomes narrow, the breakpoint forces:

#![allow(unused)]
fn main() {
collapsed = true
}

This fundamentally changes behavior.

Instead of a split layout, it becomes a navigation stack model.

Visual layout:

[ Sidebar ]
   ↓ select city
[ Content ]

Collapsed mobile navigation layout

Figure: Mobile layout where the sidebar and content appear as separate navigation pages. Internally:

  • Only one page is visible at a time.
  • The widget transitions between sidebar and content.
  • Animations provide visual continuity.

This creates a mobile-style navigation flow.

4. The collapsed Property Explained

The collapsed property determines layout strategy.

Example of setting collapse behavior via breakpoint:

#![allow(unused)]
fn main() {
bp.add_setter(&split, "collapsed", Some(&true.to_value()));
}

Meaning:

If breakpoint condition is met → force split view into stacked mode.

Important:

Developers do not manually detect window resize events. Libadwaita handles this automatically.

This reduces complexity significantly.

5. The show_content Property Explained

When in collapsed mode, we need to control which page is visible.

This is done using:

#![allow(unused)]
fn main() {
split.set_show_content(true);
}

or

#![allow(unused)]
fn main() {
split.set_show_content(false);
}

Meaning:

  • false → show sidebar
  • true → show content

This property is ignored in expanded mode.

6. Navigation Flow Logic

In mobile mode:

  1. App starts with sidebar visible.
  2. User selects a city.
  3. Application sets show_content = true.
  4. Weather panel becomes visible.

Back navigation:

#![allow(unused)]
fn main() {
split.set_show_content(false);
}

This returns the user to the sidebar.

This creates predictable navigation without manual page stacks.

7. Why Not Use GtkPaned?

GTK provides GtkPaned for split layouts.

However:

GtkPanedNavigationSplitView
Static layout onlyAdaptive layout
No automatic collapseAutomatic collapse
No stack navigationBuilt-in stack model
Manual resize logicDeclarative breakpoint
Not mobile-focusedDesigned for GNOME adaptive apps

For Linux mobile onboarding, NavigationSplitView demonstrates modern best practices.

8. Interaction With HeaderBar

In collapsed mode:

  • Navigation arrows appear.
  • They toggle show_content.

Example logic:

#![allow(unused)]
fn main() {
next_btn.connect_clicked(move |_| {
    split.set_show_content(true);
});
}

and

#![allow(unused)]
fn main() {
back_btn.connect_clicked(move |_| {
    split.set_show_content(false);
});
}

In expanded mode:

  • Arrows are hidden.
  • Both panels are visible.
  • Navigation buttons are unnecessary.

This dynamic visibility improves UX clarity.

9. Internal State Model

The widget internally manages:

  • Layout mode (collapsed)
  • Visible page (show_content)
  • Transition animations
  • Page metadata (via NavigationPage)

This creates a clean separation between:

  • Navigation layer (sidebar)
  • Content layer (weather panel)

Developers only interact with high-level properties.

They do not manage animation stacks manually.

10. Developer Onboarding Perspective

From a learning standpoint, this widget teaches:

  • Adaptive UI thinking
  • Separation of concerns
  • Declarative property-driven design
  • Mobile-first architecture

It removes the need to teach beginners:

  • Window resize signals
  • Manual layout recalculations
  • Complex conditional rendering

This reduces onboarding difficulty.

11. Architectural Significance for the Thesis

The use of NavigationSplitView demonstrates:

  • Modern Libadwaita design patterns
  • Adaptive GNOME application architecture
  • A single codebase targeting desktop and mobile
  • Clean UI separation

It serves as a concrete case study of:

How structured UI components reduce cognitive load in developer onboarding.

Responsive Design Adaptive Layout in GTK + Libadwaita

Modern Linux applications must work on:

  • Desktop environments
  • Tablets
  • Linux mobile systems

Instead of writing separate layouts for each device type, the Weather App uses Libadwaita’s adaptive system.

1. What Is Responsive Design in GTK?

Responsive design means:

The interface adapts automatically depending on available window size.

Instead of detecting “desktop vs mobile”, the application reacts to actual width constraints.

This is more flexible because:

  • Users can resize windows manually
  • Tablets vary in size
  • Desktop users may use small windows

2. The Breakpoint Concept

Libadwaita introduces the concept of a Breakpoint.

A breakpoint defines:

A condition under which a UI property should change.

In this project, the breakpoint controls whether the split view collapses.

3. Minimal Code Snippet Defining a Breakpoint

The Weather App defines a breakpoint like this:

#![allow(unused)]
fn main() {
let bp = Breakpoint::new(
    BreakpointCondition::new_length(
        BreakpointConditionLengthType::MaxWidth,
        600.0,
        LengthUnit::Sp,
    )
);
}

What this means:

  • If the window width is ≤ 600sp
  • Then a layout change should occur

Sp means a scalable unit that adapts better to different screen sizes and display settings.

In simple terms:

  • It is not just a fixed pixel value
  • It helps the interface behave more consistently across devices
  • It is more suitable for adaptive UI design than raw pixels

This is why Sp is used here instead of px.

4. Applying the Breakpoint

The breakpoint is connected to the split view:

#![allow(unused)]
fn main() {
bp.add_setter(&split, "collapsed", Some(&true.to_value()));
}

This means:

When the condition is met → set collapsed = true.

So:

  • Wide window → collapsed = false
  • Narrow window → collapsed = true

No manual resize handling is required.

5. What Happens When Collapsed?

When collapsed = true:

  • Sidebar and content are no longer shown side by side
  • Only one page is visible at a time
  • Navigation behaves like a stack

The visible page is controlled by:

#![allow(unused)]
fn main() {
split.set_show_content(true);
}

or

#![allow(unused)]
fn main() {
split.set_show_content(false);
}

This determines:

  • false → show sidebar
  • true → show content

6. Expanded Mode (Desktop)

When window width > 600sp:

  • Both panels are visible simultaneously
  • No navigation arrows are required
  • show_content becomes irrelevant

Layout looks like:

[ Sidebar | Weather Panel ]

Expanded desktop layout

Figure: Expanded desktop layout where the sidebar and weather panel are visible at the same time.

This supports efficient multitasking.

7. Collapsed Mode (Mobile)

When window width ≤ 600sp:

Layout becomes:

[ Sidebar ]
   ↓
[ Weather Panel ]

Collapsed mobile navigation

Figure: Collapsed mobile layout where only one panel is visible and navigation arrows allow switching between sidebar and content.

Only one page at a time is visible.

Navigation arrows are shown in the header bar.

This improves:

  • Touch usability
  • Focus
  • Clarity

8. Why This Matters for Developer Onboarding

For beginners, responsive design is often complex because it typically requires:

  • CSS media queries (web)
  • Separate UI files
  • Manual resize detection
  • Conditional layout logic

Libadwaita simplifies this to:

  • Define breakpoint
  • Attach property change
  • Let framework handle transitions

This reduces cognitive load.

New developers can focus on:

  • Application logic
  • Data handling
  • UX decisions

Instead of fighting layout complexity.

9. UX Implications of Responsiveness

Responsive behavior in this app ensures:

  • No wasted space on desktop
  • No cramped interface on mobile
  • Clear navigation transitions
  • Consistent user experience

Additionally:

  • Navigation arrows are hidden in expanded mode
  • Forward navigation is disabled if no city exists
  • Empty state is shown appropriately

This prevents user confusion.

10. Architectural Benefits

This design:

  • Demonstrates adaptive UI principles
  • Encourages modern GNOME development practices
  • Aligns with Libadwaita philosophy
  • Supports Linux mobile compatibility
  • Keeps code maintainable

It also proves that:

A single Rust + GTK codebase can target both desktop and mobile effectively.

Sidebar Layout Structure and Design in the Weather App

The sidebar in the Weather App acts as the navigation layer of the application.

Its responsibility is not to display weather data. Instead, it manages:

  • Searching for cities
  • Adding new cities
  • Displaying saved cities
  • Showing an empty state when no cities exist

The sidebar is built as a vertical layout container.

Creating the Sidebar Container

In the Weather App, the sidebar is defined as:

#![allow(unused)]
fn main() {
let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 12);
set_margins(&sidebar, 12);
}

A vertical Box is used because the elements must appear from top to bottom:

  1. Search row
  2. Empty state or city list

The spacing (12) ensures visual separation between elements. Margins improve readability and follow GNOME spacing guidelines.

Search Row (Search + Add Button)

At the top of the sidebar, a horizontal layout is created:

#![allow(unused)]
fn main() {
let search_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);

let search = gtk::SearchEntry::new();
search.set_hexpand(true);

let add_city_btn = gtk::Button::from_icon_name("list-add-symbolic");
}

This row groups:

  • A SearchEntry
  • A “+” button

The search field expands horizontally, while the add button stays compact.

Placing them together improves usability because:

  • Search and add actions are conceptually related
  • Users immediately understand where to interact The append calls also make the widget hierarchy explicit. They show that both controls belong to the same top interaction row inside the sidebar.

Empty State Using StatusPage

When no cities are stored, the sidebar displays a StatusPage:

#![allow(unused)]
fn main() {
let sidebar_empty = StatusPage::new();
sidebar_empty.set_title("Add first city");
sidebar_empty.set_description(Some(
    "Hit '+' to add a city to display the weather"
));
}

This prevents the sidebar from appearing blank.

Instead of showing an empty list, the app guides the user.

The empty state is toggled using logic like:

#![allow(unused)]
fn main() {
empty_page.set_visible(is_empty);
scroller.set_visible(!is_empty);
}

This ensures that only one of the following is visible:

  • StatusPage (when empty)
  • City list (when cities exist)

This improves clarity and prevents confusion.

City List Container

Saved cities are displayed inside a ListBox:

#![allow(unused)]
fn main() {
let city_list = gtk::ListBox::new();
city_list.set_selection_mode(gtk::SelectionMode::None);
}

Each city becomes an ActionRow:

#![allow(unused)]
fn main() {
let row = ActionRow::new();
row.set_title(city);
row.set_subtitle("Tap to view weather");
}

Using ActionRow provides:

  • Clean GNOME styling
  • Built-in title/subtitle layout
  • Easy click handling

The list is placed inside a scrollable container:

#![allow(unused)]
fn main() {
let scroller = gtk::ScrolledWindow::new();
scroller.set_child(Some(&city_list));
}

This ensures the sidebar remains usable even with many cities.

Dynamic Sidebar Updates

Whenever cities change, the sidebar rebuilds:

#![allow(unused)]
fn main() {
clear_listbox(&city_list);
}

Then it re-adds rows from the updated state.

This keeps the UI consistent with stored data.

The sidebar does not directly manage weather logic. It only triggers actions when a city is selected.

Design Decisions

The sidebar follows a clear design structure:

  • Top section → interaction (search + add)
  • Middle section → dynamic content (empty or list)
  • Scrollable list for scalability
  • No duplicated add buttons
  • Clear empty-state guidance

This separation makes the code easier to maintain.

It also teaches beginners:

  • How to structure UI layers
  • How to toggle visibility properly
  • How to separate navigation from content logic

Architectural Role

Within the application hierarchy:

NavigationSplitView
    ├── Sidebar (Navigation)
    └── Content (Weather)

The sidebar acts purely as a navigation controller.

It does not fetch weather data itself. It signals which city should be displayed.

This separation improves modularity and onboarding clarity.

Search Entry City Lookup and Suggestion Popover

The search entry in the Weather App allows users to search for cities dynamically.

Instead of requiring users to type the full city name correctly, the application provides live suggestions using the Open-Meteo geocoding API. This improves usability and reduces input errors. In the sidebar, the search entry is placed together with the add city button. This means the top interaction area supports both:

  • Searching for cities
  • Adding a city explicitly

This keeps the interface compact and easy to understand.

Creating the Search Entry

The Weather App creates the search field like this:

#![allow(unused)]
fn main() {
let search = gtk::SearchEntry::new();
search.set_hexpand(true);
search.set_placeholder_text(Some("Search city…"));
}

SearchEntry is used instead of a normal Entry because:

  • It is visually optimized for search tasks
  • It includes built-in styling
  • It integrates well with GNOME design patterns

The field expands horizontally to fill available space inside the sidebar.

Adding the Add City Button

Next to the search field, the Weather App creates an add button:

#![allow(unused)]
fn main() {
let add_city_btn = gtk::Button::from_icon_name("list-add-symbolic");
}

This button gives users a direct way to add a city.

Using an icon button keeps the interface minimal and matches GNOME design conventions. The plus symbol clearly communicates the purpose of the action.

Combining Search Entry and Add Button in One Row

The search entry and add button are placed together inside a horizontal layout:

#![allow(unused)]
fn main() {
let search_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);

search_row.append(&search);
search_row.append(&add_city_btn);
}

This creates a single interaction row at the top of the sidebar.

The order of the append() calls is important:

  • search_row.append(&search); places the search field first
  • search_row.append(&add_city_btn); places the add button next to it

So the layout appears like this:

[ SearchEntry | Add Button ]

This arrangement improves usability because:

  • Search and add actions are closely related
  • Users can immediately see where to type
  • Users can also add a city directly from the same row

The search field expands, while the add button remains compact.

Suggestion Popover Structure

When the user types, suggestions appear in a popover.

The structure is:

#![allow(unused)]
fn main() {
let suggestion_list = gtk::ListBox::new();

let suggestion_popover = gtk::Popover::new();
suggestion_popover.set_parent(&search);
suggestion_popover.set_child(Some(&suggestion_list));
}

This creates:

  • A floating popover
  • Attached directly to the search field
  • Containing a list of suggestion rows

This avoids navigating away from the sidebar.

The suggestion UI remains lightweight and contextual.

Reacting to User Input

The Weather App listens to changes in the search field:

#![allow(unused)]
fn main() {
search.connect_search_changed(move |s| {
    let q = s.text().to_string();
});
}

Whenever the text changes:

  • The current query is captured
  • If the text is too short, suggestions are hidden
  • Otherwise, a background request is triggered

This prevents unnecessary API calls.

Fetching Suggestions (Background Thread)

To avoid freezing the UI, the API call runs in a separate thread:

#![allow(unused)]
fn main() {
thread::spawn(move || {
    let res = fetch_geocoding_suggestions(&q_clone, 5);
});
}

This ensures:

  • The UI remains responsive
  • The search field does not lag
  • Users can continue typing

GTK’s main thread is never blocked.

Updating the UI Safely

After receiving suggestions, the UI is updated using a timeout polling mechanism:

#![allow(unused)]
fn main() {
glib::timeout_add_local(Duration::from_millis(80), move || {
    match rx.try_recv() {
        ...
    }
});
}

This allows:

  • Safe UI updates from the main thread
  • Controlled checking of background results
  • Smooth suggestion appearance

This design respects GTK’s single-threaded UI model.

Displaying Suggestions

When results are returned, the list is rebuilt:

#![allow(unused)]
fn main() {
clear_listbox(&suggestion_list);

for item in items {
    let row = ActionRow::new();
    row.set_title(&item.name);
    suggestion_list.append(&row);
}
}

Each suggestion:

  • Is displayed as an ActionRow
  • Can be clicked
  • Updates the search field
  • Triggers weather fetch

This keeps the interaction intuitive.

Handling No Results

If no city is found:

#![allow(unused)]
fn main() {
row.set_title("No city found");
row.set_subtitle("Try another spelling");
}

This avoids silent failure.

The user receives immediate feedback.

Why This Design Is Important

This search system demonstrates:

  • Real-time UI interaction
  • Safe multi-threading in GTK
  • Controlled UI updates
  • Clean separation between network logic and UI logic

From an onboarding perspective, it teaches:

  • Event-driven programming
  • Background processing in Rust
  • GTK-safe UI updates
  • User-friendly feedback handling

Architectural Flow

Search workflow in the Weather App:

  1. User types into SearchEntry
  2. search_changed signal triggers
  3. Background thread calls geocoding API
  4. Main thread updates suggestion popover
  5. User selects a suggestion
  6. Weather fetch begins

This flow is modular and easy to follow.

City List Displaying and Managing Saved Cities

The city list in the Weather App represents the core navigation element of the sidebar.

Each saved city appears as an interactive row. When a user selects a city, the weather panel updates accordingly.

The list is dynamic and reflects the current stored data.

Creating the List Container

The list is implemented using a ListBox:

#![allow(unused)]
fn main() {
let city_list = gtk::ListBox::new();
city_list.add_css_class("boxed-list");
city_list.set_selection_mode(gtk::SelectionMode::None);
}

ListBox is used because:

  • It supports vertical stacking of rows
  • It integrates well with GNOME styling
  • It allows interactive row behavior

The selection mode is disabled because row activation is handled manually.

Adding Cities as ActionRow

Each city is displayed as an ActionRow:

#![allow(unused)]
fn main() {
let row = ActionRow::new();
row.set_title(city);
row.set_subtitle("Tap to view weather");
row.set_activatable(true);
}

Using ActionRow provides:

  • Clear title + subtitle layout
  • Built-in click interaction
  • Consistent GNOME visual design

The subtitle explains what happens when the row is selected.

This improves usability for new users.

Handling Row Activation

When a city row is clicked, it triggers weather fetching:

#![allow(unused)]
fn main() {
row.connect_activated(move |_| {
    start_fetch(city_name.clone());
});
}

This keeps responsibilities separate:

  • Sidebar → navigation action
  • Weather panel → data display
  • Fetch logic → background processing

The row does not directly update UI. It only triggers the weather fetch process.

Adding Delete Button to Each Row

Each city row also contains a delete button:

#![allow(unused)]
fn main() {
let del_btn = gtk::Button::from_icon_name("user-trash-symbolic");
row.add_suffix(&del_btn);
}

The delete button:

  • Is visually attached to the row
  • Does not interfere with row activation
  • Provides direct control over stored cities

This improves user control and flexibility.

Delete Logic

When the delete button is clicked:

#![allow(unused)]
fn main() {
cities.borrow_mut()
    .retain(|c| !c.eq_ignore_ascii_case(&city_to_delete));

save_cities_to_csv(&cities.borrow());
}

This performs two actions:

  1. Removes the city from the in-memory list
  2. Updates the CSV file for persistence

The application state and storage remain synchronized.

Rebuilding the List

After changes, the list is rebuilt:

#![allow(unused)]
fn main() {
clear_listbox(&city_list);
}

Then all current cities are re-added.

This ensures:

  • UI always reflects current state
  • No outdated rows remain
  • Order remains consistent

Rebuilding is simple and predictable, which helps beginners understand the flow.

Interaction With Empty State

After deleting a city, the app checks whether the list is empty:

#![allow(unused)]
fn main() {
update_sidebar_empty_state(&cities, &city_list, &scroller, &empty_page);
}

If no cities remain:

  • The list disappears
  • The empty state becomes visible

This maintains visual consistency.

Architectural Separation

The city list handles:

  • Displaying stored cities
  • Triggering weather fetch
  • Deleting entries

It does not:

  • Fetch weather directly
  • Parse JSON
  • Manage threading

This separation keeps the architecture clean.

Sidebar = navigation controller Content panel = data display layer

Why This Design Is Important

This implementation demonstrates:

  • State-driven UI updates
  • Proper separation of logic
  • Safe shared state handling (Rc<RefCell>)
  • Persistence synchronization

From an onboarding perspective, it teaches:

  • How UI reflects internal data
  • How to manage shared mutable state in Rust
  • How to connect user interaction to background processes

Empty State Guiding the User When No Cities Exist

An empty state is shown when the application has no saved cities.

Instead of displaying a blank sidebar, the Weather App uses a StatusPage to guide the user.

This improves clarity and prevents confusion during first launch.

Creating the Empty State

In the Weather App, the empty state is created like this:

#![allow(unused)]
fn main() {
let sidebar_empty = StatusPage::new();
sidebar_empty.set_title("Add first city");
sidebar_empty.set_description(Some(
    "Hit '+' to add a city to display the weather"
));
sidebar_empty.set_vexpand(true);
}

This creates a centered message inside the sidebar.

The message:

  • Explains what to do
  • Mentions the "+" button
  • Avoids showing a useless blank space

The layout feels intentional instead of unfinished.

Why Not Show an Empty List?

If only a ListBox were shown:

  • The sidebar would look broken
  • Users might think something failed
  • No guidance would be provided

The empty state solves this by:

  • Communicating clearly
  • Reducing onboarding friction
  • Improving first-time experience

This is especially important for beginner users.

Toggling Between Empty State and List

The Weather App switches visibility depending on whether cities exist.

Simplified logic:

#![allow(unused)]
fn main() {
let is_empty = cities.borrow().is_empty();

empty_page.set_visible(is_empty);
scroller.set_visible(!is_empty);
}

This ensures:

  • When no cities exist → show StatusPage
  • When cities exist → show scrollable list

Only one element is visible at a time.

This avoids layout conflicts and overlapping widgets.

Clearing the List When Empty

When the list becomes empty, the app also clears the ListBox:

#![allow(unused)]
fn main() {
clear_listbox(&city_list);
}

This prevents:

  • Old rows from remaining in memory
  • Visual inconsistencies
  • Incorrect state representation

The UI always reflects the real data state.

Interaction With Navigation

The empty state also affects navigation behavior.

When there are no cities:

  • Forward navigation is disabled
  • Content panel remains empty

This prevents the user from navigating to a meaningless weather screen.

This logic improves consistency across the entire layout.

UX Perspective

The empty state demonstrates good UI practice:

  • Always explain what the user should do
  • Avoid blank areas
  • Provide actionable guidance
  • Connect instructions to visible UI elements

Instead of overwhelming the user, the interface gently instructs them.

Architectural Role

Within the sidebar structure:

Sidebar Box
    ├── Search Row
    ├── StatusPage (if empty)
    └── City List (if not empty)

The empty state is part of the navigation layer.

It does not perform logic itself.

It simply reflects application state.

This separation keeps the design clean and maintainable.

Why This Matters for Developer Onboarding

For new developers, this feature teaches:

  • How to reflect application state in UI
  • How to toggle widget visibility correctly
  • Why UX decisions matter in technical design
  • How small improvements improve overall quality

It shows that building applications is not only about functionality, but also about user guidance.

Weather Panel and Forecast List

The Weather Panel is the right side of the Weather App. It displays the weather information for the city selected in the sidebar.

This panel has two responsibilities:

  • Show the current weather conditions
  • Show the 7-day forecast list

However, when the application first starts, no city is selected, so no weather data should be visible.

To handle this situation cleanly, the Weather App uses a state-based UI layout.

Managing Panel States with GtkStack

The weather panel uses a GtkStack to switch between two states.

#![allow(unused)]
fn main() {
let content_stack = gtk::Stack::new();
content_stack.set_hexpand(true);
content_stack.set_vexpand(true);
}

The stack contains two pages:

#![allow(unused)]
fn main() {
content_stack.add_named(&right_empty, Some("empty"));
content_stack.add_named(&content, Some("weather"));
content_stack.set_visible_child_name("empty");
}

These represent two possible UI states.

StateMeaning
emptyNo city has been selected
weatherWeather data is available

When the application starts, the empty page is shown. After weather data is successfully fetched, the UI switches to the weather page.

Building the Weather Layout

Inside the weather page, the interface is built using a vertical layout container.

#![allow(unused)]
fn main() {
let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
set_margins(&content, 18);
}

This container organizes the weather widgets in a vertical order.

The layout contains:

  • Weather icon
  • City title
  • Weather description
  • Current temperature
  • Forecast heading
  • Forecast list

Each component has a specific role in presenting the weather information.

Displaying the City Name

The selected city name is shown using a label.

#![allow(unused)]
fn main() {
let city_title = gtk::Label::new(Some(""));
city_title.add_css_class("title-1");
city_title.set_halign(gtk::Align::Center);
}

When weather data is applied, the label is updated dynamically:

#![allow(unused)]
fn main() {
city_title.set_text(&format!("Weather in {name}"));
}

This demonstrates how UI elements react to updated application data.

Showing Current Weather

The current weather is displayed using a combination of an icon, a description label, and a temperature label.

For example, the temperature label is created as follows:

#![allow(unused)]
fn main() {
let now_temp = gtk::Label::new(Some(""));
now_temp.add_css_class("title-4");
now_temp.set_halign(gtk::Align::Center);
}

When the weather fetch completes, the UI updates:

#![allow(unused)]
fn main() {
now_temp.set_text(&format!("Now: {:.1} °C", now));
}

The weather condition code returned by the API is converted into a text description and icon before being displayed.

This ensures the UI shows user-friendly information rather than raw numeric codes.

Forecast List

Below the current temperature, the Weather App displays the 7-day forecast.

The forecast is implemented using a ListBox.

#![allow(unused)]
fn main() {
let forecast_list = gtk::ListBox::new();
forecast_list.add_css_class("boxed-list");
forecast_list.set_selection_mode(gtk::SelectionMode::None);
}

The ListBox widget is suitable for this purpose because it allows multiple rows stacked vertically with GNOME styling.

Rows are not interactive, so selection mode is disabled.

Rebuilding the Forecast List

Each time new weather data is fetched, the forecast list is rebuilt.

First, existing rows are removed:

#![allow(unused)]
fn main() {
clear_listbox(&forecast_list);
}

This step is important because it prevents previous forecast entries from remaining visible.

After clearing the list, new rows are created for each forecast day.

A simplified version of the row creation looks like this:

#![allow(unused)]
fn main() {
let row = ActionRow::new();
row.set_title(&date);
row.set_subtitle(&format!(
    "{}  •  Max {:.1}°C  Min {:.1}°C",
    weather_text,
    max,
    min
));

forecast_list.append(&row);
}

Each row represents one forecast day and displays:

  • the forecast date
  • the weather description
  • the maximum temperature
  • the minimum temperature

An icon corresponding to the weather condition is also added to the row.

Where the Forecast Data Comes From

The forecast list receives its data from the weather fetch function.

The function returns structured forecast information in the form:

#![allow(unused)]
fn main() {
Vec<(String, f64, f64, i32)>
}

Each tuple contains:

  • the forecast date
  • the maximum temperature
  • the minimum temperature
  • the weather code

The UI layer simply loops through this data and creates a row for each day.

This design ensures that the UI only displays data and does not calculate it.

Applying Weather Data to the Panel

When the background fetch completes successfully, the weather panel is updated.

First, the panel switches from the empty state to the weather state.

#![allow(unused)]
fn main() {
content_stack.set_visible_child_name("weather");
}

Then the labels and forecast list are updated using the returned data.

This dynamic update ensures that the UI always reflects the latest weather information.

Handling Errors in the Panel

If an error occurs during the weather request, the panel displays an informative message.

For example:

#![allow(unused)]
fn main() {
now_temp.set_text("No city found. Try another spelling.");
forecast_label.set_visible(false);
forecast_list.set_visible(false);
}

This ensures that:

  • the user sees a clear error message
  • outdated forecast rows are hidden
  • the interface remains consistent

Handling errors visually is an important part of creating a user-friendly application.

Expected Behavior

When the Weather App runs, the user should observe the following behavior.

At startup:

  • the weather panel shows an empty state

After selecting a city:

  • the city name appears
  • the current temperature appears
  • the weather description and icon appear
  • the 7-day forecast list appears

If an invalid city is entered:

  • a message is shown instead of weather data

This confirms that the panel correctly reacts to changes in application state.

Data Fetching: From City Name to Weather Forecast

The Weather Application retrieves real-time weather information from the Open-Meteo API. However, weather services typically do not accept city names directly when requesting weather data. Instead, they require geographic coordinates, specifically latitude and longitude.

For this reason, the Weather App performs the data retrieval process in two stages:

  1. Convert the city name into geographic coordinates using the Open-Meteo Geocoding API.
  2. Use those coordinates to request weather information from the Open-Meteo Forecast API.

This two-step design reflects how many modern weather systems operate and allows the application to separate responsibilities clearly.

1 Geocoding: Converting City Names to Coordinates

When a user enters a city such as:

Berlin

the weather API cannot directly use the text value. Instead, it requires precise coordinates, for example:

latitude  = 52.52
longitude = 13.41

Therefore, the application first sends the city name to the geocoding service, which translates the human-readable location into machine-readable coordinates.

Constructing the Geocoding Request

The geocoding request URL is constructed dynamically based on the user's input.

A simplified version of the request construction is shown below.

#![allow(unused)]
fn main() {
let url = format!(
    "https://geocoding-api.open-meteo.com/v1/search?name={}&count={}",
    urlencoding::encode(q),
    count
);
}

The parameters included in this request are:

  • name – the city entered by the user
  • count – the maximum number of search results
  • urlencoding::encode() – ensures spaces and special characters are safely encoded for URLs

This encoding step is important because city names such as New York contain spaces that must be safely transmitted over HTTP.

Geocoding Response Models

The JSON response returned by the geocoding API is mapped into Rust structures using Serde.

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct GeoResponse {
    results: Option<Vec<GeoCity>>,
}

#[derive(Debug, Deserialize, Clone)]
struct GeoCity {
    name: String,
    latitude: f64,
    longitude: f64,
}
}

These models mirror the structure of the API response.

Each GeoCity object contains:

  • the city name
  • its latitude
  • its longitude

Mapping JSON data into typed Rust structures ensures that the application can work with the data safely and predictably.

Selecting the Matching City

After the geocoding request completes, the application selects the first matching city from the results.

#![allow(unused)]
fn main() {
let city0 = geo.get(0)
    .cloned()
    .ok_or_else(|| "City not found".to_string())?;
}

If a result exists, its coordinates are used. If no result is returned, the function generates an error instead of continuing with invalid data.

This prevents runtime crashes and ensures robust behavior.

2 Fetching Weather Data

Once the geographic coordinates are available, the application can request weather data from the forecast API.

The coordinates are inserted into the request URL.

#![allow(unused)]
fn main() {
let weather_url = format!(
"https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&current=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&forecast_days=7&timezone=auto",
city0.latitude,
city0.longitude
);
}

This request retrieves:

  • the current temperature
  • the current weather condition code
  • the maximum temperature for each day
  • the minimum temperature for each day
  • the weather condition codes for the forecast
  • a 7-day forecast

The timezone=auto parameter ensures the forecast is returned in the correct local time.

Weather Response Models

The JSON returned by the weather API is also converted into Rust data structures.

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct WeatherResponse {
    current: Option<CurrentWeather>,
    daily: Option<DailyWeather>,
}
}

The current weather model is defined as:

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct CurrentWeather {
    temperature_2m: f64,
    weather_code: i32,
}
}

The daily forecast model is defined as:

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct DailyWeather {
    time: Vec<String>,
    temperature_2m_max: Vec<f64>,
    temperature_2m_min: Vec<f64>,
    weather_code: Vec<i32>,
}
}

These structures allow the program to access weather values directly using Rust field names instead of manually parsing JSON.

Sending the Weather Request

The weather request is sent using the reqwest library.

#![allow(unused)]
fn main() {
let w: WeatherResponse = reqwest::blocking::get(&weather_url)?
    .json()?;
}

This code performs several actions automatically:

  1. Sends an HTTP request to the Open-Meteo API
  2. Receives a JSON response
  3. Converts the JSON into the WeatherResponse structure

This automatic conversion is handled by Serde.

Extracting Weather Information

After the JSON response is converted into Rust data, the application extracts the current weather and forecast values.

For example:

#![allow(unused)]
fn main() {
let current = w.current.ok_or_else(|| "No current weather".to_string())?;
}

This safely retrieves the current weather data while handling the possibility that the API response might be incomplete.

Similarly, the daily forecast is accessed through:

#![allow(unused)]
fn main() {
let daily = w.daily.ok_or_else(|| "No daily forecast".to_string())?;
}

The application then loops through the forecast arrays to build the data used by the user interface.

#![allow(unused)]
fn main() {
for i in 0..n {
    rows.push((
        daily.time[i].clone(),
        daily.temperature_2m_max[i],
        daily.temperature_2m_min[i],
        daily.weather_code[i],
    ));
}
}

Each entry represents one day in the forecast.

Integration with the User Interface

The data returned from the fetch function is passed to the UI layer.

The function returns structured weather information in the following format:

#![allow(unused)]
fn main() {
Ok((city0.name, current.temperature_2m, current.weather_code, rows))
}

The graphical interface then uses this data to update:

  • the current weather display
  • the weather icon
  • the seven-day forecast list

The UI itself does not interact directly with the API. Instead, it only receives processed data from the fetch logic.

This separation improves readability, maintainability, and overall application design.

JSON Models and Error Handling

The Weather App communicates with the Open-Meteo API to retrieve weather information. The API returns data in JSON format, which must be converted into structured Rust types before the application can use it.

At the same time, network requests and external APIs may fail. Therefore, the Weather App must also implement proper error handling to avoid crashes and provide meaningful feedback to the user.

This chapter explains:

  1. How JSON responses are converted into Rust structs
  2. How the application safely handles errors during API requests

Mapping JSON Data to Rust Structures

When the Weather API returns data, the response looks conceptually like this:

{
  "current": {
    "temperature_2m": 6.3,
    "weather_code": 53
  },
  "daily": {
    "time": ["2026-03-13", "2026-03-14"],
    "temperature_2m_max": [11.8, 5.4],
    "temperature_2m_min": [5.4, 4.0],
    "weather_code": [61, 51]
  }
}

Rust cannot directly work with this raw JSON text. Instead, we define Rust structs that mirror the JSON structure.

This process is called data modeling.

Weather Response Model

The top-level JSON response is mapped into the following Rust structure:

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct WeatherResponse {
    current: Option<CurrentWeather>,
    daily: Option<DailyWeather>,
}
}

Important details:

  • Deserialize allows the Serde library to convert JSON into Rust structs.
  • Option is used because the API may sometimes omit fields.
  • WeatherResponse acts as the container for all weather data.

Current Weather Model

The current weather information is stored in a separate struct.

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct CurrentWeather {
    temperature_2m: f64,
    weather_code: i32,
}
}

Field explanation:

FieldMeaning
temperature_2mCurrent temperature at 2 meters above ground
weather_codeNumeric code representing the weather condition

The weather code is later converted into a human-readable description and icon in the user interface.

Daily Forecast Model

The seven-day forecast is represented using arrays.

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct DailyWeather {
    time: Vec<String>,
    temperature_2m_max: Vec<f64>,
    temperature_2m_min: Vec<f64>,
    weather_code: Vec<i32>,
}
}

Each index represents one forecast day.

For example:

time[0]
temperature_2m_max[0]
temperature_2m_min[0]
weather_code[0]

Together these values describe the first day in the forecast.

This design allows the application to loop through the arrays and create the forecast rows displayed in the UI.

Converting JSON into Rust Objects

After defining the models, the API response can be automatically converted using reqwest and serde.

A simplified version of the code is:

#![allow(unused)]
fn main() {
let w: WeatherResponse = reqwest::blocking::get(&weather_url)
    .map_err(|e| format!("Weather request failed: {e}"))?
    .json()
    .map_err(|e| format!("Weather JSON failed: {e}"))?;
}

This performs several steps:

  1. Send an HTTP request to the weather API
  2. Receive a JSON response
  3. Convert the JSON into WeatherResponse

After conversion, the application can safely access:

#![allow(unused)]
fn main() {
w.current
w.daily
}

No manual JSON parsing is required.

Why Option Is Important

Notice that the fields inside WeatherResponse are wrapped in Option.

#![allow(unused)]
fn main() {
current: Option<CurrentWeather>,
daily: Option<DailyWeather>,
}

This is important because:

  • APIs may return incomplete responses
  • network failures may produce partial data
  • certain fields might be missing

Using Option prevents the application from crashing and allows missing values to be handled safely.

Error Handling in the Weather App

External API requests are not always successful. Errors may occur when:

  • the user enters an invalid city name
  • the internet connection fails
  • the API returns incomplete data

The Weather App handles these situations using Rust’s Result type.

Returning Result from Fetch Functions

The weather fetch function returns a Result:

#![allow(unused)]
fn main() {
fn fetch_open_meteo(city: &str)
    -> Result<(String, f64, i32, Vec<(String, f64, f64, i32)>), String>
}

This means:

  • Ok(...) → weather data was successfully retrieved
  • Err(String) → an error occurred

Using Result forces the program to handle failures explicitly.

Handling Invalid Input

Before sending a request, the application checks whether the user entered a valid city.

#![allow(unused)]
fn main() {
if q.is_empty() {
    return Err("Empty city".into());
}
}

If the input is empty, the function immediately returns an error instead of sending an invalid API request.

Handling Missing Cities

If the geocoding API does not return a city result, the application handles it safely.

#![allow(unused)]
fn main() {
let city0 = geo.get(0)
    .cloned()
    .ok_or_else(|| "City not found".to_string())?;
}

If no matching city exists, the function returns an error rather than accessing invalid data.

This prevents crashes caused by invalid indexing.

Handling Network Errors

The HTTP request itself may fail.

For example, the internet connection might be unavailable.

The following code converts request errors into readable messages:

#![allow(unused)]
fn main() {
reqwest::blocking::get(&weather_url)
    .map_err(|e| format!("Weather request failed: {e}"))?
}

The ? operator immediately returns the error if the request fails.

This keeps the code concise and easy to read.

Handling Errors in the UI

The Weather App performs API requests in a background thread. When the request finishes, the UI checks the result.

#![allow(unused)]
fn main() {
Ok(Err(e)) => {
    show_error(e);
}
}

The UI then displays the message to the user.

For example:

#![allow(unused)]
fn main() {
now_temp.set_text(&msg);
forecast_label.set_visible(false);
forecast_list.set_visible(false);
}

This ensures:

  • the user receives a clear error message
  • outdated forecast data is removed
  • the interface remains consistent

The application continues running instead of crashing.

Handling “City Not Found” Separately

The application also detects specific errors.

#![allow(unused)]
fn main() {
if e.to_lowercase().contains("city not found") {
    show_not_found();
}
}

This allows the UI to display a more user-friendly message when a city does not exist.

Different errors can therefore produce different UI responses.

Dialogs and User Feedback (MessageDialog)

Modern applications must not fail silently.When a user performs an invalid action, the application should provide clear feedback.

In this Weather App, we use MessageDialog from Libadwaita to inform the user when:

  • The city name is empty
  • The city already exists in the list

MessageDialog is a modal dialog window that appears on top of the main window. It temporarily blocks interaction with the application until the user responds.

This ensures that the user understands what went wrong.

Why We Use MessageDialog

Imagine the user clicks Add without typing anything.If nothing happens, the user becomes confused.Instead, we show a dialog that clearly explains the problem.

This improves:

  • Usability
  • Error handling
  • User experience
  • Professional interface behavior

Dialogs are part of interaction design, not just layout design.

Example: Showing a Dialog for Empty Input

Below is a simplified version of how the dialog is created in the Weather App.

#![allow(unused)]
fn main() {
let dialog = MessageDialog::builder()
    .transient_for(&parent_window)
    .modal(true)
    .heading("Empty city name")
    .body("Please enter a valid city name.")
    .build();

dialog.add_response("ok", "OK");
dialog.present();
}

What happens here

  • MessageDialog::builder() creates a new dialog.
  • .transient_for(&parent_window) attaches it to the main window.
  • .modal(true) blocks interaction until the dialog is closed.
  • .heading() sets the main title.
  • .body() provides the explanation message.
  • .present() displays the dialog.

The dialog now appears on top of the main window.

Example: Duplicate City Warning

If the user tries to add a city that already exists, we show another dialog:

#![allow(unused)]
fn main() {
let dialog = MessageDialog::builder()
    .transient_for(&parent_window)
    .modal(true)
    .heading("City already exists")
    .body("This city is already in your saved list.")
    .build();

dialog.add_response("ok", "OK");
dialog.present();
}

This prevents duplicate entries and maintains clean data.

How MessageDialog Fits Into the UI Structure

MessageDialog is different from layout widgets like GTK Box or NavigationSplitView.

It is not part of the permanent UI structure.Instead, it appears temporarily when needed.

So in our app architecture:

  • Layout widgets structure the interface.
  • Content widgets display data.
  • Interactive widgets respond to actions.
  • Dialog widgets provide feedback.

This completes the full interaction cycle.

What Beginners Should Understand

MessageDialog teaches an important concept:

A good application does not just display information. It communicates with the user.Handling incorrect input is part of building professional software. Even small applications should:

  • Validate user input
  • Inform users about mistakes
  • Prevent silent failures

Adding Cities with a Modal Dialog and Validation

In the Weather App, users can add a new city using the “+” button in the sidebar. When the button is clicked, a small popup window appears asking the user to enter a city name.This popup is called a modal dialog.

A modal dialog temporarily blocks the main window until the user finishes the task. The user must either add the city or cancel the action before returning to the main interface. This design keeps the interaction simple and prevents accidental actions.

Creating the Modal Dialog

The add-city window is created using a secondary ApplicationWindow configured as a modal dialog.

#![allow(unused)]
fn main() {
let dialog = AdwWindow::builder()
    .application(&app)
    .title("Add City")
    .transient_for(&parent)
    .modal(true)
    .default_width(360)
    .default_height(160)
    .build();
}

Important parts:

  • transient_for(&parent) connects the dialog to the main window.
  • modal(true) prevents interaction with the main window.
  • The dialog behaves like a temporary input window.

This ensures the user focuses only on entering the city name.

Adding the Input Field

Inside the dialog, a vertical layout is created.

#![allow(unused)]
fn main() {
let content = gtk::Box::new(gtk::Orientation::Vertical, 12);

let entry = gtk::Entry::new();
entry.set_placeholder_text(Some("Enter city name…"));
content.append(&entry);
}

This creates a text input field where the user can type the city name. The placeholder text helps the user understand what to enter.

Adding Action Buttons

Two buttons are added to the dialog:

#![allow(unused)]
fn main() {
let cancel_btn = gtk::Button::with_label("Cancel");
let add_btn = gtk::Button::with_label("Add");

add_btn.add_css_class("suggested-action");
}

The Cancel button closes the dialog without saving anything. The Add button performs the main action. The CSS class suggested-action highlights the Add button so the user can easily recognize the primary action.

Reading the User Input

When the user clicks Add, the application reads the text entered in the input field.

#![allow(unused)]
fn main() {
let name = entry.text().trim().to_string();
}

The .trim() function removes extra spaces before and after the text.

For example:

"   Berlin   "

becomes:

"Berlin"

This keeps the stored data clean.

Validation: Preventing Empty Input

Before saving the city, the application checks whether the input is empty.

#![allow(unused)]
fn main() {
if name.is_empty() {
    let msg = MessageDialog::builder()
        .transient_for(&parent)
        .modal(true)
        .heading("Empty city name")
        .body("Please enter a valid city name.")
        .build();

    msg.add_response("ok", "OK");
    msg.present();
    return;
}
}

If the user tries to submit an empty value, a message dialog appears. The dialog informs the user that a valid city name must be entered. This prevents invalid data from entering the application.

Validation: Preventing Duplicate Cities

The application also checks whether the city already exists.

#![allow(unused)]
fn main() {
if cities.borrow().iter().any(|c| c.eq_ignore_ascii_case(&name)) {
    let msg = MessageDialog::builder()
        .transient_for(&parent)
        .modal(true)
        .heading("City already exists")
        .body("This city is already in your list.")
        .build();

    msg.add_response("ok", "OK");
    msg.present();
    return;
}
}

Here the function eq_ignore_ascii_case compares city names without considering uppercase or lowercase letters.

This means:

  • "Berlin"
  • "berlin"

are treated as the same city. This keeps the city list clean and prevents duplicates.

Saving the City

Only after passing the validation checks is the city stored.

#![allow(unused)]
fn main() {
cities.borrow_mut().push(name);

save_cities_to_csv(&cities.borrow());

rebuild_list();

dialog.close();
}

These steps perform the following actions:

  1. Add the city to the in-memory list
  2. Save the list to the CSV file
  3. Refresh the sidebar city list
  4. Close the dialog

This ensures the UI and stored data remain synchronized.

Why This Design Is Important

Using a modal dialog with validation helps maintain a clean and reliable application.

It ensures that:

  • users focus on one task at a time
  • invalid input is prevented
  • duplicate data is avoided
  • the application state remains consistent

This pattern is commonly used in many desktop applications.

What Freshers Learn from This

This implementation helps beginners understand several important concepts:

  • creating secondary windows in GTK
  • connecting dialogs to a parent window
  • reading user input from widgets
  • validating input before saving data
  • updating the UI after user actions

In the Weather App, the modal dialog collects the city name, validates it, updates the application state, and then closes. This demonstrates how user interaction, validation, data storage, and UI updates work together in a Rust + GTK application.

CSV Storage and Persistence

The Weather App allows users to save cities such as Berlin, Munich, These cities should remain available even after the application is closed.

To achieve this, the Weather App stores city names in a simple CSV file.

Instead of using a database, the app uses a lightweight text file. This makes the storage system easy to understand and implement, especially for beginners.

CSV File Structure

The storage file is defined using a constant:

#![allow(unused)]
fn main() {
const CSV_FILE: &str = "saved_cities.csv";
}

This file stores one city per line.

Example:

Berlin
Munich
Paris

There are no commas or extra columns. Each line simply represents one city name.

This simple structure makes the file:

  • easy to read
  • easy to debug
  • easy to edit manually if needed

Why a Simple CSV Design Is Used

The Weather App only needs to store city names.

It does not store:

  • latitude
  • longitude
  • timestamps
  • weather data

These values are fetched dynamically from the weather API when needed. Because of this, a simple text file is enough. This design helps beginners understand persistence without needing databases or complex file formats.

In-Memory Storage

While the application is running, the cities are stored in memory using a vector:

Vec<String>

Example:

["Berlin", "Munich", "Paris"]

This vector is the main working data structure used by the application.

The CSV file is only used to save and restore this data when the app starts or closes.

Loading Cities When the App Starts

When the Weather App starts, it reads the CSV file and loads the cities into memory.

#![allow(unused)]
fn main() {
fn load_cities_from_csv() -> Vec<String> {
    if !Path::new(CSV_FILE).exists() {
        return vec![];
    }

    let Ok(text) = fs::read_to_string(CSV_FILE) else {
        return vec![];
    };

    let mut out = Vec::new();

    for line in text.lines() {
        let city = line.trim();

        if !city.is_empty() &&
           !out.iter().any(|c| c.eq_ignore_ascii_case(city)) {
            out.push(city.to_string());
        }
    }

    out
}
}

This function performs several steps. First, it checks if the file exists. If the file does not exist, the application simply starts with an empty list. Next, the file is read as plain text. Each line represents a city. The function removes extra spaces and avoids duplicates before adding cities to the vector. At the end, the function returns the list of cities stored in memory.

Saving Cities After Changes

Whenever a user adds or removes a city, the CSV file must be updated.

The Weather App saves the cities using this function:

#![allow(unused)]
fn main() {
fn save_cities_to_csv(cities: &Vec<String>) {
    let mut buf = String::new();

    for c in cities {
        buf.push_str(c);
        buf.push('\n');
    }

    let _ = fs::write(CSV_FILE, buf);
}
}

This function converts the vector of cities into plain text. Each city is written on its own line. The entire file is rewritten every time the list changes. This approach keeps the logic simple because the file always matches the current state of the vector.

How Storage Works in the Weather App

The Weather App follows a simple data flow.

1. Application Start

The CSV file is read and converted into a vector.

CSV file → Vec<String>

2. User Adds or Removes Cities

The vector is updated.

Vec<String> updated

3. File Is Updated

The vector is written back to the CSV file.

Vec<String> → CSV file

This means:

  • The vector in memory is the active state
  • The CSV file only stores the data permanently

Connection to the Sidebar

The sidebar list in the Weather App displays the cities stored in the vector.

When a user adds a city:

  1. The city is added to the vector
  2. The CSV file is updated
  3. The sidebar list is rebuilt

When a user deletes a city:

  1. The city is removed from the vector
  2. The CSV file is updated
  3. The sidebar refreshes

This keeps the UI and storage synchronized.

Why This Design Is Good for Beginners

This storage system is intentionally simple.

It teaches beginners how persistence works without introducing complex technologies.

From this example, a beginner learns how to:

  • store application data in a file
  • read file content at startup
  • keep data in memory using vectors
  • update the file when data changes

The storage logic is also separated from the UI logic. The CSV functions only handle reading and writing data. They do not interact with GTK widgets.

Threading and Safe UI Updates

When the user searches for a city in the Weather App, the application sends a request to the Open-Meteo API to fetch weather data.

Network requests take time. If the request runs directly in the GTK main thread, the entire UI will freeze until the response arrives.

To prevent this, the Weather App uses background threads.

Why Threading Is Needed

GTK applications run using a single main UI thread.

This thread is responsible for:

  • drawing the window
  • updating widgets
  • processing clicks
  • handling events

If a slow operation runs on this thread, the UI cannot update.

For example:

#![allow(unused)]
fn main() {
let result = fetch_open_meteo(&query);
}

If this code runs directly when a button is clicked, the application will freeze until the network request finishes.

During this time:

  • the window cannot be resized
  • buttons do not respond
  • the interface appears stuck

To avoid this problem, long operations must run in separate threads.

Running the Weather Request in a Thread

The Weather App runs the API request inside a background thread using Rust’s thread::spawn.

#![allow(unused)]
fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    let res = fetch_open_meteo(&query);
    let _ = tx.send(res);
});
}

This code does three things:

  1. Creates a channel (tx, rx) for communication
  2. Starts a new background thread
  3. Runs the weather request inside that thread

When the request finishes, the result is sent back through the channel.

Because the network request runs in another thread, the UI remains responsive.

Why the move Keyword Is Used

The thread uses this closure:

#![allow(unused)]
fn main() {
move || {
    ...
}
}

The move keyword transfers ownership of variables into the thread.

This is required because the thread may run longer than the current scope. Rust prevents unsafe borrowing by forcing the data to be moved into the thread.

Sometimes data must be cloned before spawning the thread:

#![allow(unused)]
fn main() {
let q_clone = query.clone();
}

Then q_clone is moved into the thread.

This prevents data races and keeps memory safe.

Why Channels Are Used

Background threads should not update GTK widgets directly.

GTK requires all UI updates to happen on the main thread.

So the worker thread only sends the result:

#![allow(unused)]
fn main() {
tx.send(res);
}

The main thread receives the data and updates the UI safely.

This separation prevents crashes and undefined behavior.

Receiving the Result in the Main Thread

To receive results safely, the Weather App checks the channel using:

#![allow(unused)]
fn main() {
glib::timeout_add_local(Duration::from_millis(80), move || {
    match rx.try_recv() {
        ...
    }
});
}

This function runs inside the GTK main event loop.

It periodically checks:

  • whether the background thread has finished
  • whether new data is available

If data is ready, the UI can be updated.

If not, the loop continues waiting.

This approach keeps the interface responsive.

Updating the UI Safely

When the result is received, the main thread updates the interface.

Example:

#![allow(unused)]
fn main() {
apply_weather(name, now, rows);
}

Inside that function, widgets are updated:

#![allow(unused)]
fn main() {
city_title.set_text(&format!("Weather in {name}"));
now_temp.set_text(&format!("Now: {:.1} °C", now));
}

This is safe because it runs in the GTK main thread.

The worker thread never touches UI widgets directly.

Handling Errors

If the weather request fails, the UI can also show an error message.

Example:

#![allow(unused)]
fn main() {
show_error(e);
}

This may update labels or hide forecast widgets.

Again, this happens only in the main thread.

Architecture of the Weather Fetch System

The Weather App separates responsibilities clearly:

User searches city
        ↓
Background thread fetches weather
        ↓
Result sent through channel
        ↓
Main GTK thread receives data
        ↓
UI is updated

Each component has a single responsibility:

  • Thread → performs slow work
  • Channel → transfers data safely
  • Main thread → updates the UI

This architecture keeps the application stable and responsive.

What Freshers Should Learn

From this design, beginners learn several important ideas:

  • GTK uses a single UI thread
  • Slow operations must run in background threads
  • Rust threads are created using thread::spawn
  • The move keyword transfers ownership safely
  • Threads communicate using channels
  • UI updates must happen only in the main thread

Flatpak Packaging and Sandbox Model

To distribute the Weather App in a clean and reliable way, the application is packaged using Flatpak. Flatpak uses a manifest file that explains how the application should be built, which runtime environment it needs, and what permissions it requires.

In this project, the manifest file is named:

com.example.Weather.yaml

This file works like a build instruction file. It tells Flatpak how to build the Rust project, which GNOME environment should be used, and how the application should run inside a sandbox.

Flatpak is very useful for GTK4 and Libadwaita applications because it provides the same runtime environment on every Linux system. This means the Weather App does not depend on the libraries installed on the user’s computer. Instead, it runs in a controlled environment defined by the manifest.

Why Flatpak Is Used

During development, the Weather App can be run using:

cargo run

In this case, the program uses the local system libraries. This is convenient for development, but it can cause problems when the project is moved to another computer.

For example, another system might have:

  • a different GTK version
  • missing dependencies
  • incompatible libraries

Flatpak solves this problem by packaging the application with a fixed runtime environment.

For the Weather App, Flatpak ensures that:

  • the correct GNOME runtime is used
  • Rust compilation works correctly
  • the application can access the internet for API requests
  • the application can store user data such as saved_cities.csv

Because of this, Flatpak is a good solution for distributing GTK applications.

The Flatpak Manifest File

The main file used for packaging is:

com.example.Weather.yaml

This manifest file defines:

  • the application identity
  • the runtime and SDK
  • the Rust extension
  • the build commands
  • the project source
  • the permissions given to the application

In simple terms, this file tells Flatpak everything needed to build and run the Weather App.

Application Identity

At the top of the manifest file, the application ID is defined:

id: com.example.Weather

This ID uniquely identifies the application in the Flatpak system.

The ID is important because it connects to:

  • desktop integration
  • installation directories
  • the sandbox identity of the application

It should be the same or very similar to the application ID used inside the GTK application code.

Runtime and SDK

The runtime and SDK define the environment used to build and run the application.

runtime: org.gnome.Platform
runtime-version: '49'
sdk: org.gnome.Sdk

The runtime provides the libraries needed when the application runs. For GTK applications, this includes GTK, Libadwaita, and other system libraries.

The SDK provides the development environment. It includes:

  • compilers
  • development tools
  • build dependencies

So the runtime is used when the app runs, and the SDK is used when the app is built.

This makes sure the Weather App works the same on every system.

Rust SDK Extension

Since the Weather App is written in Rust, the build environment also needs the Rust compiler.

This is added with the Rust SDK extension:

sdk-extensions:
  - org.freedesktop.Sdk.Extension.rust-stable

This extension provides:

  • the Rust compiler
  • Cargo package manager

inside the Flatpak build environment.

Without this extension, commands like cargo build would not work during the Flatpak build process.

Build Commands

The build process is defined inside the modules section of the manifest.

The Weather App is built using Cargo:

build-commands:
  - cargo build --release --verbose
  - install -Dm755 target/release/weatherapp /app/bin/weatherapp

The first command compiles the Rust project in release mode.

The second command copies the compiled program into:

/app/bin/weatherapp

Inside Flatpak, /app/bin is the standard location for application executables.

This step turns the Rust project into an installable application inside the Flatpak environment.

Source Definition

The manifest also defines the project source:

sources:
  - type: dir
    path: .

This tells Flatpak to use the current project directory as the source.

Flatpak copies the project files into its own isolated build environment and then runs the build commands there.

This helps create a clean and reproducible build because the result does not depend on the developer’s local system configuration.

Permissions and Finish Arguments

Flatpak applications run inside a sandbox. By default, this sandbox blocks most system access.

The application cannot automatically access:

  • the internet
  • the display server
  • the user’s home directory

To allow necessary features, permissions must be declared in the manifest.

These permissions are defined using finish-args:

finish-args:
  - --share=network
  - --share=ipc
  - --socket=wayland
  - --socket=fallback-x11
  - --filesystem=home

These permissions allow the Weather App to use certain system resources.

For example:

  • --share=network allows internet access
  • --share=ipc allows communication between processes
  • --socket=wayland allows connection to Wayland display server
  • --socket=fallback-x11 allows fallback support for X11
  • --filesystem=home allows access to the user’s home directory

Flatpak requires developers to explicitly declare these permissions.

How This Connects to the Weather App

The Weather App runs normally with cargo run. However, when it is packaged with Flatpak, it runs inside a sandbox environment.

This means the application cannot automatically access system resources.

Instead, everything must be declared in the manifest.

For the Weather App, this includes:

  • network access to fetch weather data
  • display access to show the GUI window
  • filesystem access to save the CSV file containing cities

So the Flatpak manifest directly reflects the needs of the application.

Building the Weather App as a Flatpak

To make building easier, the project can include a small Makefile.

The Makefile helps automate the build process so developers do not need to type long commands manually.

The Makefile can:

  • install required runtimes
  • install the GNOME SDK
  • install the Rust extension
  • build the Flatpak package
  • run the application

This makes it easier for beginners to run the project.

Step 1 - Install the Build Tool

First install the Flatpak builder tool:

sudo apt install flatpak-builder

This tool reads the Flatpak manifest and performs the build process.

Step 2 - Install Dependencies

Inside the project directory, run:

make dep

This installs the required runtimes and SDK components.

Step 3 - Build and Run the Application

Now build the application using:

make

The Makefile will automatically install the required runtimes, build the application, and run it inside the Flatpak environment.

After the build finishes, a Libadwaita window should appear.

What the Makefile Does Internally

The Makefile internally runs commands similar to the following:

flatpak install --user org.gnome.Platform//49
flatpak install --user org.gnome.Sdk//49
flatpak install --user org.freedesktop.Sdk.Extension.rust-stable//25.08
flatpak-builder --user --install --force-clean repo com.example.Weather.yaml
flatpak run com.example.Weather

These commands install the GNOME runtime, the GNOME SDK, the Rust extension, and then build and run the application.

This is useful for developers who clone the repository and want to run the application quickly.

Understanding the Sandbox Model

When the Weather App is packaged with Flatpak, it does not run like a normal application. Instead, it runs inside a sandbox.

A sandbox is an isolated environment that limits access to the system.

This improves security and stability. Even if the application has a bug, it cannot freely access sensitive system resources.

What Sandboxing Means in Practice

By default, Flatpak applications cannot:

  • access the internet
  • access the user’s home directory
  • access many system resources

The developer must explicitly declare what the application needs.

In the Weather App, this is done using finish-args.

Network Permission

The Weather App fetches weather data from the Open-Meteo API.

To allow this, the manifest includes:

- --share=network

This permission allows the application to send HTTP requests.

Without this permission, the weather API requests would fail inside the Flatpak environment.

Display Server Access

GTK applications need access to a display server to show windows.

The Weather App includes these permissions:

- --socket=wayland
- --socket=fallback-x11

These allow the application to connect to Wayland or X11.

Without these permissions, the application window would not appear.

File System Access

The Weather App stores saved cities in a CSV file:

#![allow(unused)]
fn main() {
const CSV_FILE: &str = "saved_cities.csv";
}

To allow this file to be created and updated, the manifest includes:

- --filesystem=home

This allows the application to access the user’s home directory.

Without this permission, the application would not be able to save the CSV file when running inside Flatpak.

Why the Sandbox Model Matters

The sandbox model improves security because applications cannot access system resources unless permission is granted.

This ensures that:

  • applications cannot access sensitive files automatically
  • permissions are transparent
  • users have better control over installed applications
  • applications run in a predictable environment

The Weather App only requests the permissions it needs:

  • network access for API calls
  • display access for the GUI
  • home directory access for CSV storage

This makes the Weather App a good beginner example of how Flatpak packaging and sandbox permissions work together.

Debugging Common Errors and Flatpak Issues

While building the Weather App, several errors may appear. These errors are normal and are part of the development process, especially when working with Rust, GTK, networking, threading, and Flatpak. Instead of memorizing solutions, it is more useful to understand why these errors occur. This section explains some common issues that beginners may encounter while developing or packaging the Weather App.

Common Application Errors - Window Appears Blank

Sometimes the application runs, but the window appears empty. This usually happens if the window is not presented or if no widget is attached to the window. In the Weather App, the window is displayed using:

#![allow(unused)]
fn main() {
window.present();
}

If this line is missing, the application runs but no window appears. Another possible issue is forgetting to attach the layout to the window:

#![allow(unused)]
fn main() {
window.set_content(Some(&layout));
}

If the content is not set correctly, the window will appear but it will be empty. A beginner should always check that:

  • the window is presented
  • the window has a valid content widget

UI Freezing During Weather Fetch

If the weather data is fetched directly on the main thread, the UI may freeze.

For example, this code would block the GTK UI thread:

#![allow(unused)]
fn main() {
let result = fetch_open_meteo(&query);
}

Network requests take time, and if they run on the UI thread, the interface cannot update.

In the Weather App, this is solved using a background thread:

#![allow(unused)]
fn main() {
thread::spawn(move || {
    let res = fetch_open_meteo(&query);
    let _ = tx.send(res);
});
}

This keeps the UI responsive while the weather data is fetched in the background.

If the UI freezes, the first thing to check is whether a long-running operation is blocking the main thread.

Resource or UI File Not Found

Another common error appears when GTK resources cannot be found.

For example, the program may show an error like:

The resource does not exist

This usually means the resource path does not match the configuration.

For example, if the resource prefix in resources.xml is:

<gresource prefix="/com/example/WeatherApp">

Then the UI file must be loaded using:

#![allow(unused)]
fn main() {
"/com/example/WeatherApp/weather.ui"
}

The path must match exactly. Even small differences can cause runtime errors.

Duplicate or Missing Cities

Sometimes the city list may show duplicates.

This usually happens if the CSV loading logic does not prevent duplicates.

In the Weather App, duplicates are avoided using:

#![allow(unused)]
fn main() {
!out.iter().any(|c| c.eq_ignore_ascii_case(city))
}

If this check is removed, the same city may appear multiple times.

When debugging data problems, it is helpful to check both:

  • the in-memory city list
  • the CSV file contents

Rust Borrowing Errors

Rust may produce compile errors related to borrowing and ownership. This can happen when using shared state like:

#![allow(unused)]
fn main() {
Rc<RefCell<Vec<String>>>
}

For example, mutation must be done carefully:

#![allow(unused)]
fn main() {
cities.borrow_mut().push(name);
}

If multiple mutable borrows occur at the same time, Rust will stop compilation. Although these errors may seem confusing at first, Rust usually provides very detailed error messages explaining the problem.

Flatpak Issues

Sometimes the Weather App works correctly with:

cargo run

but behaves differently when run inside Flatpak.

This happens because Flatpak runs the application inside a sandbox environment that restricts access to system resources.

Understanding these restrictions helps beginners debug Flatpak problems.

Network Requests Not Working

The Weather App fetches weather data using network requests such as:

#![allow(unused)]
fn main() {
reqwest::blocking::get(&weather_url)
}

If the Flatpak manifest does not include the permission:

- --share=network

the sandbox blocks internet access. The application will start normally, but the API request will fail. This is one of the most common Flatpak issues.

File Storage Not Working

The Weather App saves cities using a CSV file:

#![allow(unused)]
fn main() {
const CSV_FILE: &str = "saved_cities.csv";
}

and writes data using:

#![allow(unused)]
fn main() {
fs::write(CSV_FILE, buf);
}

Inside Flatpak, file access may fail if the filesystem permission is missing.

The manifest includes:

- --filesystem=home

Without this permission, the application cannot write to the home directory. This may confuse beginners because the same code works perfectly with cargo run.

Runtime Version Problems

Another issue can occur if the required runtime version is not installed.

For example, the manifest may specify:

runtime-version: '49'

If this runtime is not installed, Flatpak may show an error such as:

Requested extension not installed

The solution is to install the runtime manually:

flatpak install flathub org.gnome.Platform//49

Resource Files Missing in Flatpak

Sometimes the application works locally but fails inside Flatpak with resource errors.

For example:

#![allow(unused)]
fn main() {
gtk::Builder::from_resource("/com/example/WeatherApp/weather.ui");
}

If the UI file was not compiled correctly into the resource bundle, Flatpak will not find it.

Flatpak builds the project in an isolated environment, so files must be included using compiled resources instead of relative file paths.

Application Window Appears but UI Is Incomplete

If the application window opens but some widgets are missing, the issue may be related to the runtime environment.

Flatpak runs the application using the runtime defined in the manifest:

runtime: org.gnome.Platform

If the application requires a GTK or Libadwaita feature that is not available in that runtime version, the UI may behave incorrectly. Ensuring that the runtime version matches the application requirements helps prevent this issue.

Why Debugging Is Important

Debugging is an important part of learning software development. The Weather App combines several technologies:

  • Rust programming
  • GTK user interface
  • networking
  • threading
  • file storage
  • Flatpak sandbox packaging

Each of these layers can introduce errors.

By understanding how these components work together, beginners can learn how to identify and fix problems more effectively. Errors are not failures. They are opportunities to understand the system more deeply and improve development skills.

Weather App Project

This Weather App was developed as part of my master thesis tutorial Developer Onboarding for LinuxMobile Platform.

The app helps beginners understand how to build a small LinuxMobile application using Rust, Libadwaita, and the Open-Meteo API.

Main Features

- Search for a city

- Show current weather

- Show 5-day forecast

- Save selected cities

- Adaptive layout for desktop and mobile

- Built with Rust and Libadwaita

Connection with the Tutorial

This Weather App is the practical example used in the tutorial. The tutorial explains the setup, user interface design, API connection, data storage, and adaptive layout step by step.

Source Code

The complete source code of the Weather App is available in a separate Codeberg repository:

[Open Weather App Source Code](https://codeberg.org/Oormila94/weather-app)

The repository contains the Rust project files, including Cargo.toml, the src folder, and the main application code used in this tutorial.