Compare commits

...

2 Commits

13 changed files with 61 additions and 109 deletions

View File

@ -14,6 +14,7 @@
"endifs",
"epaint",
"gles",
"humantime",
"imageops",
"impls",
"indexmap",
@ -34,6 +35,7 @@
"rwlock",
"rwlocks",
"smallvec",
"subsec",
"thiserror",
"turbofish",
"Undefines",

17
Cargo.lock generated
View File

@ -1563,9 +1563,19 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "humantime-serde"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c"
dependencies = [
"humantime",
"serde",
]
[[package]]
name = "hyper"
@ -4801,6 +4811,8 @@ dependencies = [
"egui",
"egui_plot",
"futures",
"humantime",
"humantime-serde",
"image",
"itertools 0.14.0",
"naga",
@ -4853,7 +4865,6 @@ dependencies = [
"pin-project",
"serde",
"serde_json",
"serde_with",
"tokio",
"tokio-stream",
"toml 0.9.5",

View File

@ -29,6 +29,8 @@ egui_wgpu_backend = "0.35.0"
egui_winit_platform = "0.27.0"
extend = "1.2.0"
futures = "0.3.31"
humantime = "2.3.0"
humantime-serde = "1.1.1"
image = "0.25.8"
include_dir = "0.7.4"
itertools = "0.14.0"

View File

@ -8,4 +8,7 @@ absolute-paths-allowed-crates = [
# Crate consists of only nondescript modules
"naga_oil",
# Some modules are named generically, so we can't import them (e.g. `egui::util`)
"egui",
]

View File

@ -13,7 +13,6 @@ image = { workspace = true }
pin-project = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
tokio = { workspace = true }
tokio-stream = { features = ["fs"], workspace = true }
toml = { workspace = true }

View File

@ -1,84 +0,0 @@
//! 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<Self, Self::Err> {
let mut time = Duration::ZERO;
// Remove any hours from the time
if let Some((hours, rest)) = s.split_once('h') {
let hours = hours
.parse::<u64>()
.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::<u64>()
.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::<f64>()
.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(())
}
}

View File

@ -29,7 +29,6 @@
)]
// Modules
pub mod duration_display;
pub mod loadable;
mod rect;
pub mod resource_manager;
@ -39,7 +38,6 @@ pub mod walk_dir;
// Exports
pub use {
duration_display::DurationDisplay,
loadable::Loadable,
rect::Rect,
resource_manager::ResourceManager,

View File

@ -21,6 +21,8 @@ directories = { workspace = true }
egui = { features = ["default_fonts"], workspace = true }
egui_plot = { workspace = true }
futures = { workspace = true }
humantime = { workspace = true }
humantime-serde = { workspace = true }
image = { workspace = true }
itertools = { workspace = true }
naga = { features = ["deserialize"], workspace = true }

View File

@ -21,7 +21,7 @@ use {
profile::Profiles,
shared::SharedWindow,
},
core::{ops::RangeInclusive, str::FromStr, time::Duration},
core::{ops::RangeInclusive, time::Duration},
egui::Widget,
std::{
collections::HashMap,
@ -30,7 +30,7 @@ use {
},
strum::IntoEnumIterator,
winit::{event_loop::EventLoopProxy, window::WindowId},
zsw_util::{AppError, DurationDisplay, Rect},
zsw_util::{AppError, Rect},
zsw_wgpu::Wgpu,
};
@ -147,9 +147,19 @@ 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)
.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)
.custom_formatter(|secs, _| {
// Note: We round any durations to the nearest millisecond to avoid displaying
// numbers that are too big
let duration = Duration::from_secs_f64(secs);
let nanos_per_ms = Duration::from_millis(1).subsec_nanos();
let duration = Duration::new(
duration.as_secs(),
duration.subsec_nanos().next_multiple_of(nanos_per_ms),
);
humantime::format_duration(duration).to_string()
})
.custom_parser(|s| humantime::parse_duration(s).ok().map(|d| d.as_secs_f64()))
.clamping(egui::SliderClamping::Never)
.ui(ui);
*duration = Duration::from_secs_f32(secs);
}

View File

@ -7,8 +7,8 @@ pub mod render_panels;
// Imports
use {
crate::{menu, metrics::FrameTimes},
core::time::Duration,
egui::{Widget, style},
core::{hash::Hash, time::Duration},
egui::{Widget, epaint, style},
std::collections::{HashMap, HashSet},
};
@ -219,11 +219,19 @@ where
},
};
egui_plot::BarChart::new(duration_idx.name(), bars)
// Note: This auto-color algorithm is based on egui's auto color, but using
// a stable value. This also makes the colors the same between runs.
let color = {
let duration_hash = egui::util::hash(duration_idx) as u16;
let h = f32::from(duration_hash) / f32::from(u16::MAX);
epaint::Hsva::new(h, 0.85, 0.5, 1.0)
};
egui_plot::BarChart::new(duration_idx.name(), bars).color(color)
}
/// Duration index
pub trait DurationIdx<T> {
pub trait DurationIdx<T>: Hash {
/// Returns the name of this index
fn name(&self) -> String;

View File

@ -14,7 +14,7 @@ pub fn draw(ui: &mut egui::Ui, render_frame_times: &mut FrameTimes<RenderFrameTi
super::draw_plot(ui, render_frame_times, &display, DurationIdx::iter());
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
#[derive(strum::EnumIter)]
enum DurationIdx {
WaitNextFrame,

View File

@ -8,7 +8,7 @@ use {
crate::{display::DisplayName, playlist::PlaylistName},
core::time::Duration,
std::{borrow::Borrow, fmt, sync::Arc},
zsw_util::{DurationDisplay, ResourceManager, resource_manager},
zsw_util::{ResourceManager, resource_manager},
};
/// Profiles
@ -90,8 +90,8 @@ impl resource_manager::FromSerialized<ProfileName, ser::Profile> for Profile {
}),
ser::ProfilePanelShader::Fade(shader) => ProfilePanelShader::Fade(ProfilePanelFadeShader {
playlists: shader.playlists.into_iter().map(PlaylistName::from).collect(),
duration: shader.duration.0,
fade_duration: shader.fade_duration.0,
duration: shader.duration,
fade_duration: shader.fade_duration,
inner: match shader.inner {
ser::ProfilePanelFadeShaderInner::Basic => ProfilePanelFadeShaderInner::Basic,
ser::ProfilePanelFadeShaderInner::White { strength } =>
@ -130,8 +130,8 @@ impl resource_manager::ToSerialized<ProfileName, ser::Profile> for Profile {
ProfilePanelShader::Fade(shader) =>
ser::ProfilePanelShader::Fade(ser::ProfilePanelFadeShader {
playlists: shader.playlists.iter().map(PlaylistName::to_string).collect(),
duration: DurationDisplay(shader.duration),
fade_duration: DurationDisplay(shader.fade_duration),
duration: shader.duration,
fade_duration: shader.fade_duration,
inner: match shader.inner {
ProfilePanelFadeShaderInner::Basic => ser::ProfilePanelFadeShaderInner::Basic,
ProfilePanelFadeShaderInner::White { strength } =>

View File

@ -1,7 +1,6 @@
//! Serialized profile
// Imports
use zsw_util::DurationDisplay;
use core::time::Duration;
/// Profile
#[derive(Debug)]
@ -46,8 +45,10 @@ pub struct ProfilePanelNoneShader {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ProfilePanelFadeShader {
pub playlists: Vec<String>,
pub duration: DurationDisplay,
pub fade_duration: DurationDisplay,
#[serde(with = "humantime_serde")]
pub duration: Duration,
#[serde(with = "humantime_serde")]
pub fade_duration: Duration,
/// Inner
#[serde(flatten)]