fjordgard

A desktop clock application
Log | Files | Refs | README | LICENSE

commit a151e8921f824d5e4990af558692bab675656295
parent b4acad021c0651512479ca419719e80a1b863a7c
Author: Sylvia Ivory <git@sivory.net>
Date:   Wed, 18 Jun 2025 18:59:14 -0700

Finish settings layout

Diffstat:
Msrc/config.rs | 4++--
Msrc/main.rs | 95+++++++++++++++++++------------------------------------------------------------
Asrc/settings.rs | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 246 insertions(+), 75 deletions(-)

diff --git a/src/config.rs b/src/config.rs @@ -26,14 +26,15 @@ impl BackgroundMode { } } +#[derive(Clone)] pub struct Location { pub longitude: f64, pub latitude: f64, pub name: Option<String>, } +#[derive(Clone)] pub struct Config { - pub timezone: String, pub time_format: String, pub background_mode: BackgroundMode, pub background: String, @@ -43,7 +44,6 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - timezone: String::from("Etc/UTC"), time_format: String::from("%-I:%M:%S"), background_mode: BackgroundMode::Unsplash, background: BackgroundMode::Unsplash.default_background().to_string(), diff --git a/src/main.rs b/src/main.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, rc::Rc}; + use chrono::{DateTime, Local}; use iced::{ Color, Element, Font, Length, Size, Subscription, Task, @@ -11,23 +13,21 @@ use iced::{ }; use background::{BackgroundKind, background}; -use config::Config; +use config::{BackgroundMode, Config}; use icon::{icon, icon_button}; use strum::VariantArray; -use crate::config::BackgroundMode; - mod background; mod config; mod icon; +mod settings; struct Fjordgard { - config: Config, + config: Rc<RefCell<Config>>, time: DateTime<Local>, background: BackgroundKind, - backgrounds: combo_box::State<BackgroundMode>, - settings_window: Option<window::Id>, + settings_window: Option<settings::Settings>, main_window: window::Id, } @@ -48,8 +48,7 @@ enum Message { MainWindowOpened, WindowClosed(window::Id), - ConfigBackgroundMode(BackgroundMode), - ConfigTimeFormat(String), + Settings(settings::Message), } impl Fjordgard { @@ -58,10 +57,9 @@ impl Fjordgard { ( Self { - config: Config::default(), + config: Rc::new(RefCell::new(Config::default())), time: Local::now(), background: BackgroundKind::Color(Color::from_rgb8(255, 255, 255)), - backgrounds: combo_box::State::new(BackgroundMode::VARIANTS.to_vec()), settings_window: None, main_window: id, @@ -91,7 +89,7 @@ impl Fjordgard { ..Default::default() }); - self.settings_window = Some(id); + self.settings_window = Some(settings::Settings::new(id, self.config.clone())); open.map(Message::SettingsOpened) } else { @@ -106,14 +104,12 @@ impl Fjordgard { Task::none() } } - Message::ConfigBackgroundMode(mode) => { - self.config.background = mode.default_background().to_string(); - self.config.background_mode = mode; - Task::none() - } - Message::ConfigTimeFormat(format) => { - self.config.time_format = format; - Task::none() + Message::Settings(msg) => { + if let Some(settings) = &mut self.settings_window { + settings.update(msg).map(Message::Settings) + } else { + Task::none() + } } _ => Task::none(), } @@ -123,12 +119,17 @@ impl Fjordgard { if self.main_window == window_id { self.view_main() } else { - self.view_settings() + self.settings_window + .as_ref() + .expect("settings window") + .view() + .map(Message::Settings) } } fn view_main(&self) -> Element<Message> { - let dt = self.time.format(&self.config.time_format); + let config = self.config.borrow(); + let dt = self.time.format(&config.time_format); let mut time_text = String::new(); if let Err(_) = dt.write_to(&mut time_text) { @@ -177,58 +178,6 @@ impl Fjordgard { .into() } - fn view_settings(&self) -> Element<Message> { - let placeholder = Config::default(); - let config = &self.config; - - let mut background_mode_row = - row![text(config.background_mode.edit_text()).width(Length::FillPortion(1)),]; - - if config.background_mode == BackgroundMode::Local { - let text = if config.background == "" { - "Select file..." - } else { - &config.background - }; - - background_mode_row = - background_mode_row.push(button(text).width(Length::FillPortion(2))); - } else { - background_mode_row = background_mode_row.push( - text_input( - config.background_mode.default_background(), - &config.background, - ) - .width(Length::FillPortion(2)), - ); - } - - container( - column![ - row![ - text("Time format").width(Length::FillPortion(1)), - text_input(&placeholder.time_format, &config.time_format) - .width(Length::FillPortion(2)) - .on_input(Message::ConfigTimeFormat) - ], - row![ - text("Background mode").width(Length::FillPortion(1)), - combo_box( - &self.backgrounds, - "", - Some(&placeholder.background_mode), - Message::ConfigBackgroundMode - ) - .width(Length::FillPortion(2)) - ], - background_mode_row - ] - .spacing(10), - ) - .padding(15) - .into() - } - fn subscription(&self) -> Subscription<Message> { Subscription::batch(vec![ time::every(time::Duration::from_secs(1)).map(|_| Message::Tick(Local::now())), diff --git a/src/settings.rs b/src/settings.rs @@ -0,0 +1,222 @@ +use std::{cell::RefCell, rc::Rc}; + +use iced::{ + Element, Length, Task, + widget::{ + button, column, combo_box, container, row, scrollable, text, text_input, vertical_space, + }, + window, +}; +use strum::VariantArray; + +use crate::config::{BackgroundMode, Config}; + +#[derive(Debug, Clone, PartialEq, strum::Display, strum::VariantArray)] +enum WeatherLocation { + Disabled, + #[strum(to_string = "Location name")] + LocationName, + Coordinates, +} + +pub struct Location { + name: String, + latitude: f64, + longitude: f64, +} + +pub struct Settings { + pub id: window::Id, + config: Rc<RefCell<Config>>, + backgrounds: combo_box::State<BackgroundMode>, + locations: combo_box::State<WeatherLocation>, + + time_format: String, + background_mode: BackgroundMode, + background: String, + + location: WeatherLocation, + name: String, + latitude: String, + longitude: String, + + location_results: Vec<Location>, +} + +#[derive(Debug, Clone)] +pub enum Message { + TimeFormat(String), + BackgroundMode(BackgroundMode), + Background(String), + Location(WeatherLocation), + Name(String), + NameSubmitted, + Latitude(String), + Longitude(String), + Save, + FileSelector, +} + +impl Settings { + pub fn new(id: window::Id, config: Rc<RefCell<Config>>) -> Self { + let original_config = config.borrow().clone(); + let location = original_config.location; + + let latitude = location + .as_ref() + .map(|l| l.latitude.to_string()) + .unwrap_or_default(); + let longitude = location + .as_ref() + .map(|l| l.longitude.to_string()) + .unwrap_or_default(); + let name = location + .as_ref() + .map(|l| l.name.clone()) + .flatten() + .unwrap_or_default(); + let location = location + .as_ref() + .map(|l| { + l.name + .as_ref() + .map(|_| WeatherLocation::LocationName) + .unwrap_or(WeatherLocation::Coordinates) + }) + .unwrap_or(WeatherLocation::Disabled); + + Self { + id, + config, + backgrounds: combo_box::State::new(BackgroundMode::VARIANTS.to_vec()), + locations: combo_box::State::new(WeatherLocation::VARIANTS.to_vec()), + + time_format: original_config.time_format, + background_mode: original_config.background_mode, + background: original_config.background, + + location, + latitude, + longitude, + name, + + location_results: vec![], + } + } + + pub fn update(&mut self, msg: Message) -> Task<Message> { + match msg { + Message::Location(location) => { + self.location = location; + Task::none() + } + Message::BackgroundMode(mode) => { + self.background = mode.default_background().to_string(); + self.background_mode = mode; + Task::none() + } + _ => Task::none(), + } + } + + pub fn view(&self) -> Element<Message> { + let mut background_mode_row = + row![text(self.background_mode.edit_text()).width(Length::FillPortion(1))]; + + if self.background_mode == BackgroundMode::Local { + let text = if self.background == "" { + "Select file..." + } else { + &self.background + }; + + background_mode_row = background_mode_row.push( + button(text) + .on_press(Message::FileSelector) + .width(Length::FillPortion(2)), + ); + } else { + background_mode_row = background_mode_row.push( + text_input(self.background_mode.default_background(), &self.background) + .on_input(Message::Background) + .width(Length::FillPortion(2)), + ); + } + + let (latitude, longitude, name) = match self.location { + WeatherLocation::Disabled => (None, None, None), + WeatherLocation::LocationName => (None, None, Some(Message::Name)), + WeatherLocation::Coordinates => { + (Some(Message::Latitude), Some(Message::Longitude), None) + } + }; + + let mut results = column![]; + + for res in self.location_results.iter() { + results = results.push( + button(text(format!( + "{} ({}, {})", + res.name, res.latitude, res.longitude + ))) + .style(button::text), + ) + } + + scrollable( + container( + column![ + row![ + text("Time format").width(Length::FillPortion(1)), + text_input("", &self.time_format) + .width(Length::FillPortion(2)) + .on_input(Message::TimeFormat) + ], + row![ + text("Background mode").width(Length::FillPortion(1)), + combo_box( + &self.backgrounds, + "", + Some(&self.background_mode), + Message::BackgroundMode + ) + .width(Length::FillPortion(2)) + ], + background_mode_row, + row![ + text("Weather Location").width(Length::FillPortion(1)), + combo_box(&self.locations, "", Some(&self.location), Message::Location) + .width(Length::FillPortion(2)) + ], + row![ + text("Latitude").width(Length::FillPortion(1)), + text_input("", &self.latitude) + .width(Length::FillPortion(2)) + .on_input_maybe(latitude) + ], + row![ + text("Longitude").width(Length::FillPortion(1)), + text_input("", &self.longitude) + .width(Length::FillPortion(2)) + .on_input_maybe(longitude) + ], + row![ + text("Location").width(Length::FillPortion(1)), + text_input("", &self.name) + .width(Length::FillPortion(2)) + .on_input_maybe(name), + ], + scrollable(results) + .height(Length::Fixed( + 64.0 * (self.location_results.len().clamp(0, 1) as f32) + )) + .width(Length::Fill), + button("Save").on_press(Message::Save) + ] + .spacing(10), + ) + .padding(15), + ) + .into() + } +}