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

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