From 310bdab36f2ca28077619d364f29e03816bbab17 Mon Sep 17 00:00:00 2001 From: Filipe Rodrigues Date: Thu, 11 Sep 2025 23:53:48 +0100 Subject: [PATCH] Durations are now parsed and displayed with possible hour, minute and second markers. --- Cargo.lock | 1 + profiles/cross-screen.toml | 4 +- profiles/default.toml | 8 +-- profiles/full.toml | 4 +- zsw-util/Cargo.toml | 1 + zsw-util/src/duration_display.rs | 84 ++++++++++++++++++++++++++++++++ zsw-util/src/lib.rs | 2 + zsw/src/profile/profiles.rs | 4 +- zsw/src/profile/ser.rs | 18 ++----- zsw/src/settings_menu.rs | 7 +-- 10 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 zsw-util/src/duration_display.rs diff --git a/Cargo.lock b/Cargo.lock index 172d6da..f7cfcbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4865,6 +4865,7 @@ dependencies = [ "pin-project", "serde", "serde_json", + "serde_with", "tokio", "tracing", ] diff --git a/profiles/cross-screen.toml b/profiles/cross-screen.toml index 9a3c1fb..25e8374 100644 --- a/profiles/cross-screen.toml +++ b/profiles/cross-screen.toml @@ -4,6 +4,6 @@ display = "cross-screen" [panels.shader] type = "fade" playlists = ["example1"] -duration = 5.0 -fade_duration = 1.0 +duration = "5s" +fade_duration = "1s" fade = "basic" diff --git a/profiles/default.toml b/profiles/default.toml index 644af3a..ba9b1ed 100644 --- a/profiles/default.toml +++ b/profiles/default.toml @@ -4,8 +4,8 @@ display = "multiple" [panels.shader] type = "fade" playlists = ["example1"] -duration = 5.0 -fade_duration = 1.0 +duration = "1h1m1.1s" +fade_duration = "1m1.1s" fade = "out" strength = 1.0 @@ -16,7 +16,7 @@ display = "quarter" [panels.shader] type = "fade" playlists = ["example1"] -duration = 5.0 -fade_duration = 1.0 +duration = "5s" +fade_duration = "1s" fade = "white" strength = 1.0 diff --git a/profiles/full.toml b/profiles/full.toml index 1bd8fa3..6be7d3e 100644 --- a/profiles/full.toml +++ b/profiles/full.toml @@ -4,6 +4,6 @@ display = "full" [panels.shader] type = "fade" playlists = ["example1"] -duration = 5.0 -fade_duration = 1.0 +duration = "5s" +fade_duration = "1s" fade = "basic" diff --git a/zsw-util/Cargo.toml b/zsw-util/Cargo.toml index fe87122..8a8e93d 100644 --- a/zsw-util/Cargo.toml +++ b/zsw-util/Cargo.toml @@ -13,6 +13,7 @@ image = { workspace = true } pin-project = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +serde_with = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/zsw-util/src/duration_display.rs b/zsw-util/src/duration_display.rs new file mode 100644 index 0000000..c6d952e --- /dev/null +++ b/zsw-util/src/duration_display.rs @@ -0,0 +1,84 @@ +//! Duration display + +// Imports +use { + crate::AppError, + app_error::Context, + core::{fmt, str::FromStr, time::Duration}, +}; + +/// Duration +#[derive(Clone, Copy, Debug)] +#[derive(serde_with::SerializeDisplay)] +#[derive(serde_with::DeserializeFromStr)] +pub struct DurationDisplay(pub Duration); + +impl FromStr for DurationDisplay { + type Err = AppError; + + fn from_str(mut s: &str) -> Result { + let mut time = Duration::ZERO; + + // Remove any hours from the time + if let Some((hours, rest)) = s.split_once('h') { + let hours = hours + .parse::() + .with_context(|| format!("Expected an integer before `h`, found {hours:?}"))?; + time += Duration::from_hours(hours); + s = rest; + } + + // Remove any minutes from the time + if let Some((mins, rest)) = s.split_once('m') { + let mins = mins + .parse::() + .with_context(|| format!("Expected an integer before `m`, found {mins:?}"))?; + time += Duration::from_mins(mins); + s = rest; + } + + // Then remove any trailing `s` the user might have added + let secs = s.strip_suffix('s').unwrap_or(s); + + // And parse the rest as seconds (may be empty) + let secs = match secs { + "" => 0.0, + _ => secs + .parse::() + .with_context(|| format!("Expected a number of seconds, found {secs:?}"))?, + }; + time += Duration::from_secs_f64(secs); + + Ok(Self(time)) + } +} + +impl fmt::Display for DurationDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hours = self.0.as_secs() / 3600; + if hours != 0 { + write!(f, "{hours}h")?; + } + + let mins = (self.0 - Duration::from_hours(hours)).as_secs() / 60; + if mins != 0 { + write!(f, "{mins}m")?; + } + + let secs = (self.0 - Duration::from_hours(hours) - Duration::from_mins(mins)).as_secs_f64(); + if secs != 0.0 || (hours == 0 && mins == 0) { + // TODO: Find some other way of having variable precision (up to millisecond) + let mut secs = format!("{secs:.3}"); + while secs.ends_with('0') { + _ = secs.pop(); + } + if secs.ends_with('.') { + _ = secs.pop(); + } + secs.push('s'); + f.pad(&secs)?; + } + + Ok(()) + } +} diff --git a/zsw-util/src/lib.rs b/zsw-util/src/lib.rs index 99ea355..32f23c9 100644 --- a/zsw-util/src/lib.rs +++ b/zsw-util/src/lib.rs @@ -24,6 +24,7 @@ )] // Modules +pub mod duration_display; pub mod loadable; mod rect; mod tuple_collect_res; @@ -32,6 +33,7 @@ pub mod walk_dir; // Exports pub use { + duration_display::DurationDisplay, loadable::Loadable, rect::Rect, tuple_collect_res::{TupleCollectRes1, TupleCollectRes2, TupleCollectRes3, TupleCollectRes4, TupleCollectRes5}, diff --git a/zsw/src/profile/profiles.rs b/zsw/src/profile/profiles.rs index 6264e89..1e5ae41 100644 --- a/zsw/src/profile/profiles.rs +++ b/zsw/src/profile/profiles.rs @@ -76,8 +76,8 @@ impl Profiles { ser::ProfilePanelShader::Fade(shader) => ProfilePanelShader::Fade(ProfilePanelFadeShader { playlists: shader.playlists.into_iter().map(PlaylistName::from).collect(), - duration: shader.duration, - fade_duration: shader.fade_duration, + duration: shader.duration.0, + fade_duration: shader.fade_duration.0, inner: match shader.inner { ser::ProfilePanelFadeShaderInner::Basic => ProfilePanelFadeShaderInner::Basic, diff --git a/zsw/src/profile/ser.rs b/zsw/src/profile/ser.rs index 3c202d8..31ab37e 100644 --- a/zsw/src/profile/ser.rs +++ b/zsw/src/profile/ser.rs @@ -1,12 +1,7 @@ //! Serialized profile -// TODO: Allow deserializing durations from strings such as "500ms", "5s", "1m2s", etc. - // Imports -use { - core::time::Duration, - serde_with::{DurationSecondsWithFrac, serde_as}, -}; +use zsw_util::DurationDisplay; /// Profile #[derive(Debug)] @@ -45,16 +40,11 @@ pub struct ProfilePanelNoneShader { /// Panel shader fade #[derive(Debug)] -#[serde_as] #[derive(serde::Serialize, serde::Deserialize)] pub struct ProfilePanelFadeShader { - pub playlists: Vec, - - #[serde_as(as = "DurationSecondsWithFrac")] - pub duration: Duration, - - #[serde_as(as = "DurationSecondsWithFrac")] - pub fade_duration: Duration, + pub playlists: Vec, + pub duration: DurationDisplay, + pub fade_duration: DurationDisplay, /// Inner #[serde(flatten)] diff --git a/zsw/src/settings_menu.rs b/zsw/src/settings_menu.rs index 743f064..5583ecd 100644 --- a/zsw/src/settings_menu.rs +++ b/zsw/src/settings_menu.rs @@ -10,12 +10,12 @@ mod panels; // Imports use { crate::{AppEvent, display::Displays, panel::Panel}, - core::{ops::RangeInclusive, time::Duration}, + core::{ops::RangeInclusive, str::FromStr, time::Duration}, egui::{Widget, mutex::Mutex}, std::{path::Path, sync::Arc}, strum::IntoEnumIterator, winit::{dpi::LogicalPosition, event_loop::EventLoopProxy}, - zsw_util::{AppError, Rect}, + zsw_util::{AppError, DurationDisplay, Rect}, zsw_wgpu::Wgpu, }; @@ -124,7 +124,8 @@ fn draw_duration(ui: &mut egui::Ui, duration: &mut Duration, range: RangeInclusi let start = range.start().as_secs_f32(); let end = range.end().as_secs_f32(); egui::Slider::new(&mut secs, start..=end) - .suffix("s") + .custom_formatter(|secs, _| DurationDisplay(Duration::from_secs_f64(secs)).to_string()) + .custom_parser(|s| DurationDisplay::from_str(s).ok().map(|d| d.0.as_secs_f64())) .clamping(egui::SliderClamping::Edits) .ui(ui); *duration = Duration::from_secs_f32(secs);