fjordgard

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

commit e9243eb18f1a6ab0b8a70931c99d7e72e36f7381
parent a151e8921f824d5e4990af558692bab675656295
Author: Sylvia Ivory <git@sivory.net>
Date:   Wed, 18 Jun 2025 20:21:28 -0700

Validate settings inputs

Diffstat:
Msrc/config.rs | 2--
Msrc/main.rs | 47++++++++++++++++++++++++++++++-----------------
Msrc/settings.rs | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
3 files changed, 131 insertions(+), 32 deletions(-)

diff --git a/src/config.rs b/src/config.rs @@ -1,5 +1,3 @@ -use std::fmt; - #[derive(Debug, Clone, PartialEq, strum::Display, strum::VariantArray)] pub enum BackgroundMode { Unsplash, diff --git a/src/main.rs b/src/main.rs @@ -1,21 +1,20 @@ use std::{cell::RefCell, rc::Rc}; -use chrono::{DateTime, Local}; +use chrono::{ + DateTime, Local, + format::{Item, StrftimeItems}, +}; use iced::{ Color, Element, Font, Length, Size, Subscription, Task, font::Weight, time, - widget::{ - button, center, column, combo_box, container, horizontal_space, row, stack, text, - text_input, - }, + widget::{center, column, container, horizontal_space, row, stack, text}, window, }; use background::{BackgroundKind, background}; -use config::{BackgroundMode, Config}; +use config::Config; use icon::{icon, icon_button}; -use strum::VariantArray; mod background; mod config; @@ -26,6 +25,8 @@ struct Fjordgard { config: Rc<RefCell<Config>>, time: DateTime<Local>, background: BackgroundKind, + format_string: String, + format_parsed: Vec<Item<'static>>, settings_window: Option<settings::Settings>, main_window: window::Id, @@ -54,12 +55,20 @@ enum Message { impl Fjordgard { fn new() -> (Self, Task<Message>) { let (id, open) = window::open(window::Settings::default()); + let config = Config::default(); + + let format_string = config.time_format.clone(); + let format_parsed = StrftimeItems::new_lenient(&format_string) + .parse_to_owned() + .unwrap(); ( Self { - config: Rc::new(RefCell::new(Config::default())), + config: Rc::new(RefCell::new(config)), time: Local::now(), background: BackgroundKind::Color(Color::from_rgb8(255, 255, 255)), + format_string, + format_parsed, settings_window: None, main_window: id, @@ -67,6 +76,7 @@ impl Fjordgard { open.map(|_| Message::MainWindowOpened), ) } + fn title(&self, window_id: window::Id) -> String { if window_id == self.main_window { String::from("Fjordgard") @@ -79,6 +89,16 @@ impl Fjordgard { match msg { Message::Tick(time) => { self.time = time; + + let config_format = &self.config.borrow().time_format; + + if &self.format_string != config_format { + self.format_string = config_format.clone(); + self.format_parsed = StrftimeItems::new_lenient(&config_format) + .parse_to_owned() + .unwrap(); + } + Task::none() } Message::OpenSettings => { @@ -128,18 +148,11 @@ impl Fjordgard { } fn view_main(&self) -> Element<Message> { - 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) { - time_text = String::from("Invalid time format") - } - let mut bold = Font::DEFAULT; bold.weight = Weight::Bold; - let time_widget = text(time_text) + let time_text = self.time.format_with_items(self.format_parsed.iter()); + let time_widget = text(time_text.to_string()) .size(100) .font(bold) .width(Length::Fill) diff --git a/src/settings.rs b/src/settings.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, rc::Rc}; use iced::{ - Element, Length, Task, + Background, Border, Color, Element, Length, Task, Theme, widget::{ button, column, combo_box, container, row, scrollable, text, text_input, vertical_space, }, @@ -106,8 +106,8 @@ impl Settings { pub fn update(&mut self, msg: Message) -> Task<Message> { match msg { - Message::Location(location) => { - self.location = location; + Message::TimeFormat(format) => { + self.time_format = format; Task::none() } Message::BackgroundMode(mode) => { @@ -115,11 +115,64 @@ impl Settings { self.background_mode = mode; Task::none() } + Message::Background(background) => { + self.background = background; + Task::none() + } + Message::Location(location) => { + self.location = location; + Task::none() + } + Message::Name(name) => { + self.name = name; + Task::none() + } + Message::Latitude(latitude) => { + self.latitude = latitude; + Task::none() + } + Message::Longitude(longitude) => { + self.longitude = longitude; + Task::none() + } _ => Task::none(), } } pub fn view(&self) -> Element<Message> { + 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 save_message = Some(Message::Save); + + let color_style = if self.background_mode == BackgroundMode::Solid + && Color::parse(&self.background).is_none() + { + save_message = None; + text_input_error + } else { + text_input::default + }; + + let latitude_style = if self.latitude.parse::<f64>().is_ok() || latitude.is_none() { + text_input::default + } else { + save_message = None; + text_input_error + }; + + let longitude_style = if self.longitude.parse::<f64>().is_ok() || longitude.is_none() { + text_input::default + } else { + save_message = None; + text_input_error + }; + let mut background_mode_row = row![text(self.background_mode.edit_text()).width(Length::FillPortion(1))]; @@ -139,18 +192,11 @@ impl Settings { background_mode_row = background_mode_row.push( text_input(self.background_mode.default_background(), &self.background) .on_input(Message::Background) - .width(Length::FillPortion(2)), + .width(Length::FillPortion(2)) + .style(color_style), ); } - 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() { @@ -193,12 +239,14 @@ impl Settings { text_input("", &self.latitude) .width(Length::FillPortion(2)) .on_input_maybe(latitude) + .style(latitude_style) ], row![ text("Longitude").width(Length::FillPortion(1)), text_input("", &self.longitude) .width(Length::FillPortion(2)) .on_input_maybe(longitude) + .style(longitude_style) ], row![ text("Location").width(Length::FillPortion(1)), @@ -211,7 +259,7 @@ impl Settings { 64.0 * (self.location_results.len().clamp(0, 1) as f32) )) .width(Length::Fill), - button("Save").on_press(Message::Save) + button("Save").on_press_maybe(save_message) ] .spacing(10), ) @@ -220,3 +268,43 @@ impl Settings { .into() } } + +fn text_input_error(theme: &Theme, status: text_input::Status) -> text_input::Style { + let palette = theme.extended_palette(); + + let active = text_input::Style { + background: Background::Color(palette.danger.weak.color), + border: Border { + radius: 2.0.into(), + width: 1.0, + color: palette.danger.strong.color, + }, + icon: palette.danger.weak.text, + placeholder: palette.danger.strong.color, + value: palette.danger.weak.text, + selection: palette.danger.strong.color, + }; + + match status { + text_input::Status::Active => active, + text_input::Status::Hovered => text_input::Style { + border: Border { + color: palette.danger.base.text, + ..active.border + }, + ..active + }, + text_input::Status::Focused => text_input::Style { + border: Border { + color: palette.background.strong.color, + ..active.border + }, + ..active + }, + text_input::Status::Disabled => text_input::Style { + background: Background::Color(palette.danger.weak.color), + value: active.placeholder, + ..active + }, + } +}