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:
- Removes the city from the in-memory list
- 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