fjordgard

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

commit 9a4456496275c4fcc7d3fe4efef2f867216bc1ae
parent 203e97dee608c460c4c5b68d8e1d9ab1621f2539
Author: Sylvia Ivory <git@sivory.net>
Date:   Thu, 19 Jun 2025 00:46:17 -0700

Allow loading local images

Diffstat:
Msrc/background.rs | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/config.rs | 8++++----
Msrc/icon.rs | 9++++++++-
Msrc/main.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/settings.rs | 5++++-
5 files changed, 160 insertions(+), 57 deletions(-)

diff --git a/src/background.rs b/src/background.rs @@ -1,12 +1,10 @@ use iced::{ - Color, Element, Length, Point, Renderer, Size, Theme, mouse, + Color, ContentFit, Element, Length, Point, Renderer, Size, Task, Theme, mouse, widget::{canvas, container, image, stack, text}, }; +use tokio::fs; -pub enum BackgroundKind { - Image(image::Handle), - Color(Color), -} +use crate::config::{BackgroundMode, Config}; struct Solid(Color); @@ -33,20 +31,99 @@ impl<Message> canvas::Program<Message> for Solid { } } -pub fn background<'a, Message: 'a>(kind: &'a BackgroundKind) -> Element<'a, Message> { - match kind { - BackgroundKind::Color(c) => canvas(Solid(*c)) +pub struct BackgroundHandle { + pub mode: BackgroundMode, + background: String, + + image_handle: Option<image::Handle>, +} + +#[derive(Debug, Clone)] +pub enum Message { + BackgroundRead(Result<Vec<u8>, String>), +} + +impl BackgroundHandle { + pub fn new(config: &Config) -> (Self, Task<Message>) { + let mut handle = Self { + mode: config.background_mode, + background: config.background.clone(), + image_handle: None, + }; + + let task = handle.refresh(); + + return (handle, task); + } + + pub fn load_config(&mut self, config: &Config) -> Task<Message> { + self.mode = config.background_mode; + self.background = config.background.clone(); + + self.refresh() + } + + fn refresh(&mut self) -> Task<Message> { + match self.mode { + BackgroundMode::Local => { + let path = self.background.clone(); + + Task::future(async move { fs::read(&path).await }) + .map(|r| Message::BackgroundRead(r.map_err(|e| e.to_string()))) + } + _ => Task::none(), + } + } + + pub fn update(&mut self, msg: Message) -> Task<Message> { + match msg { + Message::BackgroundRead(res) => match res { + // TODO; log error + Err(_) => Task::none(), + Ok(bytes) => { + self.image_handle = Some(image::Handle::from_bytes(bytes)); + Task::none() + } + }, + } + } + + fn solid<'a>(color: Color) -> Element<'a, Message> { + canvas(Solid(color)) .width(Length::Fill) .height(Length::Fill) - .into(), - BackgroundKind::Image(i) => stack![ - image(i).width(Length::Fill).height(Length::Fill), - // TODO; finish credits - container(text("Photo, John Doe, Unsplash")) - .align_left(Length::Fill) - .align_bottom(Length::Fill) - .padding(15) - ] - .into(), + .into() + } + + pub fn view(&self) -> Element<Message> { + match self.mode { + BackgroundMode::Solid => { + Self::solid(Color::parse(&self.background).unwrap_or(Color::BLACK)) + } + _ => { + if let Some(handle) = &self.image_handle { + let img = image(handle) + .content_fit(ContentFit::Cover) + .width(Length::Fill) + .height(Length::Fill); + + if self.mode == BackgroundMode::Local { + img.into() + } else { + stack![ + img, + // TODO; finish credits + container(text("Photo, John Doe, Unsplash")) + .align_left(Length::Fill) + .align_bottom(Length::Fill) + .padding(15) + ] + .into() + } + } else { + Self::solid(Color::BLACK) + } + } + } } } diff --git a/src/config.rs b/src/config.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone, PartialEq, strum::Display, strum::VariantArray)] +#[derive(Debug, Clone, Copy, PartialEq, strum::Display, strum::VariantArray)] pub enum BackgroundMode { Unsplash, Solid, @@ -10,7 +10,7 @@ impl BackgroundMode { match self { // https://unsplash.com/collections/1053828/tabliss-official Self::Unsplash => "1053828", - Self::Solid => "#ffffff", + Self::Solid => "#000000", Self::Local => "", } } @@ -43,8 +43,8 @@ impl Default for Config { fn default() -> Self { Self { time_format: String::from("%-I:%M:%S"), - background_mode: BackgroundMode::Unsplash, - background: BackgroundMode::Unsplash.default_background().to_string(), + background_mode: BackgroundMode::Solid, + background: BackgroundMode::Solid.default_background().to_string(), location: None, } } diff --git a/src/icon.rs b/src/icon.rs @@ -1,5 +1,5 @@ use iced::{ - Element, Length, + Color, Element, Length, Theme, widget::{button, svg}, }; @@ -7,6 +7,7 @@ pub fn icon<'a, Message>(handle: impl Into<svg::Handle>) -> Element<'a, Message> svg(handle) .height(Length::Fixed(16.0)) .width(Length::Fixed(16.0)) + .style(white) .into() } @@ -19,3 +20,9 @@ pub fn icon_button<'a, Message: 'a + Clone>( .on_press(on_press) .into() } + +fn white(_theme: &Theme, _status: svg::Status) -> svg::Style { + svg::Style { + color: Some(Color::WHITE), + } +} diff --git a/src/main.rs b/src/main.rs @@ -13,10 +13,12 @@ use iced::{ window, }; -use background::{BackgroundKind, background}; +use background::BackgroundHandle; use config::Config; use icon::{icon, icon_button}; +use crate::config::BackgroundMode; + mod background; mod config; mod icon; @@ -26,7 +28,7 @@ struct Fjordgard { config: Rc<RefCell<Config>>, meteo: Arc<MeteoClient>, time: DateTime<Local>, - background: BackgroundKind, + background: BackgroundHandle, format_string: String, format_parsed: Vec<Item<'static>>, @@ -47,11 +49,12 @@ enum Message { Media(MediaControl), OpenSettings, - SettingsOpened(window::Id), + SettingsOpened, MainWindowOpened, WindowClosed(window::Id), Settings(settings::Message), + Background(background::Message), } impl Fjordgard { @@ -65,20 +68,24 @@ impl Fjordgard { .unwrap(); let meteo = MeteoClient::new(None).unwrap(); + let (background, task) = BackgroundHandle::new(&config); ( Self { config: Rc::new(RefCell::new(config)), meteo: Arc::new(meteo), time: Local::now(), - background: BackgroundKind::Color(Color::from_rgb8(255, 255, 255)), + background, format_string, format_parsed, settings_window: None, main_window: id, }, - open.map(|_| Message::MainWindowOpened), + Task::batch(vec![ + open.map(|_| Message::MainWindowOpened), + task.map(Message::Background), + ]), ) } @@ -95,15 +102,6 @@ impl Fjordgard { 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 => { @@ -119,7 +117,7 @@ impl Fjordgard { self.meteo.clone(), )); - open.map(Message::SettingsOpened) + open.map(|_| Message::SettingsOpened) } else { Task::none() } @@ -132,6 +130,21 @@ impl Fjordgard { Task::none() } } + Message::Settings(settings::Message::Committed) => { + let config = self.config.borrow(); + let config_format = &config.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(); + } + + self.background + .load_config(&config) + .map(Message::Background) + } Message::Settings(msg) => { if let Some(settings) = &mut self.settings_window { settings.update(msg).map(Message::Settings) @@ -139,6 +152,7 @@ impl Fjordgard { Task::none() } } + Message::Background(msg) => self.background.update(msg).map(Message::Background), _ => Task::none(), } } @@ -163,36 +177,38 @@ impl Fjordgard { let time_widget = text(time_text.to_string()) .size(100) .font(bold) + .color(Color::WHITE) .width(Length::Fill) .center(); let weather_widget = container(row![ icon("icons/weather/not-available.svg"), horizontal_space().width(Length::Fixed(7.25)), - text("Weather unknown") + text("Weather unknown").color(Color::WHITE) ]) .center_x(Length::Fill); - let control = container( - row![ - icon_button("icons/previous.svg", Message::Media(MediaControl::Previous)), - icon_button("icons/pause.svg", Message::Media(MediaControl::Pause)), - icon_button("icons/next.svg", Message::Media(MediaControl::Next)), - ] - .spacing(5), - ) - .center_x(Length::Fill); - let settings = icon_button("icons/settings.svg", Message::OpenSettings); + let mut main_column = column![settings, center(column![time_widget, weather_widget])]; + + if self.background.mode == BackgroundMode::Unsplash { + main_column = main_column.push( + container( + row![ + icon_button("icons/previous.svg", Message::Media(MediaControl::Previous)), + icon_button("icons/pause.svg", Message::Media(MediaControl::Pause)), + icon_button("icons/next.svg", Message::Media(MediaControl::Next)), + ] + .spacing(5), + ) + .center_x(Length::Fill), + ) + } + stack![ - background(&self.background), - container(column![ - settings, - center(column![time_widget, weather_widget]), - control - ]) - .padding(15) + self.background.view().map(Message::Background), + container(main_column).padding(15) ] .height(Length::Fill) .width(Length::Fill) diff --git a/src/settings.rs b/src/settings.rs @@ -60,6 +60,8 @@ pub enum Message { FileSelector, FileSelected(Option<FileHandle>), Save, + + Committed, } impl Settings { @@ -229,8 +231,9 @@ impl Settings { } } - Task::none() + Task::done(Message::Committed) } + _ => Task::none(), } }