From a4a6871e07a958175a4d37493b4c1ea692579a50 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:15:20 +0100 Subject: [PATCH 01/20] Fix: replace theme-window when switching themes --- crates/kas-core/src/action.rs | 8 ++++++++ crates/kas-core/src/app/window.rs | 9 ++++++++- crates/kas-core/src/theme/config.rs | 2 +- crates/kas-core/src/theme/multi.rs | 2 +- crates/kas-core/src/theme/simple_theme.rs | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/kas-core/src/action.rs b/crates/kas-core/src/action.rs index 7681a9034..4605dacb0 100644 --- a/crates/kas-core/src/action.rs +++ b/crates/kas-core/src/action.rs @@ -42,6 +42,8 @@ bitflags! { /// Resize all widgets in the window const RESIZE = 1 << 9; /// Update theme memory + /// + /// Implies [`Action::RESIZE`]. #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] const THEME_UPDATE = 1 << 10; @@ -49,6 +51,12 @@ bitflags! { #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] const EVENT_CONFIG = 1 << 11; + /// Switch themes, replacing theme-window instances + /// + /// Implies [`Action::RESIZE`]. + #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] + #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] + const THEME_SWITCH = 1 << 12; /// Reconfigure all widgets of the window /// /// *Configuring* widgets assigns [`Id`](crate::Id) identifiers and calls diff --git a/crates/kas-core/src/app/window.rs b/crates/kas-core/src/app/window.rs index cd99b81bf..81b9db5eb 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -312,7 +312,13 @@ impl> Window { } else if action.contains(Action::UPDATE) { self.update(state); } - if action.contains(Action::THEME_UPDATE) { + if action.contains(Action::THEME_SWITCH) { + if let Some(ref mut window) = self.window { + let scale_factor = window.scale_factor() as f32; + window.theme_window = state.shared.theme.new_window(scale_factor); + } + action |= Action::RESIZE; + } else if action.contains(Action::THEME_UPDATE) { if let Some(ref mut window) = self.window { let scale_factor = window.scale_factor() as f32; state @@ -320,6 +326,7 @@ impl> Window { .theme .update_window(&mut window.theme_window, scale_factor); } + action |= Action::RESIZE; } if action.contains(Action::RESIZE) { if let Some(ref mut window) = self.window { diff --git a/crates/kas-core/src/theme/config.rs b/crates/kas-core/src/theme/config.rs index 2d3891e12..128e9ea11 100644 --- a/crates/kas-core/src/theme/config.rs +++ b/crates/kas-core/src/theme/config.rs @@ -222,7 +222,7 @@ impl Config { #[allow(clippy::float_cmp)] pub fn apply_config(&mut self, other: &Config) -> Action { let action = if self.font_size != other.font_size { - Action::RESIZE | Action::THEME_UPDATE + Action::THEME_UPDATE } else if self != other { Action::REDRAW } else { diff --git a/crates/kas-core/src/theme/multi.rs b/crates/kas-core/src/theme/multi.rs index 9ae76f332..dff49d4c9 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -190,7 +190,7 @@ impl ThemeControl for MultiTheme { if let Some(index) = self.names.get(theme).cloned() { if index != self.active { self.active = index; - return Action::RESIZE | Action::THEME_UPDATE; + return Action::THEME_SWITCH; } } Action::empty() diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index ab770449d..37d3cdb5d 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -159,7 +159,7 @@ where impl ThemeControl for SimpleTheme { fn set_font_size(&mut self, pt_size: f32) -> Action { self.config.set_font_size(pt_size); - Action::RESIZE | Action::THEME_UPDATE + Action::THEME_UPDATE } fn active_scheme(&self) -> &str { From 9ada0e56ffe1200a05529d62f7706363afa90cd6 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:16:50 +0100 Subject: [PATCH 02/20] Replace Theme::Config with a fixed type --- crates/kas-core/src/config.rs | 3 +- crates/kas-core/src/theme/flat_theme.rs | 6 ++-- crates/kas-core/src/theme/mod.rs | 2 +- crates/kas-core/src/theme/multi.rs | 15 ++------- crates/kas-core/src/theme/simple_theme.rs | 6 ++-- crates/kas-core/src/theme/theme_dst.rs | 37 +++++------------------ crates/kas-core/src/theme/traits.rs | 9 ++---- crates/kas-wgpu/src/shaded_theme.rs | 6 ++-- 8 files changed, 22 insertions(+), 62 deletions(-) diff --git a/crates/kas-core/src/config.rs b/crates/kas-core/src/config.rs index 4da8e7956..d8cb364e0 100644 --- a/crates/kas-core/src/config.rs +++ b/crates/kas-core/src/config.rs @@ -307,7 +307,8 @@ impl Options { match self.config_mode { #[cfg(feature = "serde")] ConfigMode::Read | ConfigMode::ReadWrite if self.theme_config_path.is_file() => { - let config: T::Config = Format::guess_and_read_path(&self.theme_config_path)?; + let config: crate::theme::Config = + Format::guess_and_read_path(&self.theme_config_path)?; config.apply_startup(); // Ignore Action: UI isn't built yet let _ = theme.apply_config(&config); diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index 8db9adf70..dd49cd7ba 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -105,16 +105,14 @@ impl Theme for FlatTheme where DS::Draw: DrawRoundedImpl, { - type Config = Config; type Window = dim::Window; - type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { >::config(&self.base) } - fn apply_config(&mut self, config: &Self::Config) -> Action { + fn apply_config(&mut self, config: &Config) -> Action { >::apply_config(&mut self.base, config) } diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs index 75e80f521..e431d0535 100644 --- a/crates/kas-core/src/theme/mod.rs +++ b/crates/kas-core/src/theme/mod.rs @@ -38,7 +38,7 @@ pub use simple_theme::SimpleTheme; pub use size::SizeCx; pub use style::*; pub use text::{SizableText, Text}; -pub use theme_dst::{MaybeBoxed, ThemeDst}; +pub use theme_dst::ThemeDst; pub use traits::{Theme, ThemeConfig, ThemeControl, Window}; #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] pub use {draw::ThemeDraw, size::ThemeSize}; diff --git a/crates/kas-core/src/theme/multi.rs b/crates/kas-core/src/theme/multi.rs index dff49d4c9..f2e5685b6 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -78,23 +78,14 @@ impl MultiThemeBuilder { } impl Theme for MultiTheme { - type Config = Config; type Window = Box; - type Draw<'a> = Box; - fn config(&self) -> std::borrow::Cow { - let boxed_config = self.themes[self.active].config(); - // TODO: write each sub-theme's config instead of this stupid cast! - let config: Config = boxed_config - .as_ref() - .downcast_ref::() - .unwrap() - .clone(); - std::borrow::Cow::Owned(config) + fn config(&self) -> std::borrow::Cow { + self.themes[self.active].config() } - fn apply_config(&mut self, config: &Self::Config) -> Action { + fn apply_config(&mut self, config: &Config) -> Action { let mut action = Action::empty(); for theme in &mut self.themes { action |= theme.apply_config(config); diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 37d3cdb5d..dcf644172 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -86,16 +86,14 @@ impl Theme for SimpleTheme where DS::Draw: DrawRoundedImpl, { - type Config = Config; type Window = dim::Window; - type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { std::borrow::Cow::Borrowed(&self.config) } - fn apply_config(&mut self, config: &Self::Config) -> Action { + fn apply_config(&mut self, config: &Config) -> Action { let mut action = self.config.apply_config(config); if let Some(cols) = self.config.get_active_scheme() { self.cols = cols.into(); diff --git a/crates/kas-core/src/theme/theme_dst.rs b/crates/kas-core/src/theme/theme_dst.rs index a9965e368..ef832d318 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -5,32 +5,12 @@ //! Stack-DST versions of theme traits -use std::any::Any; -use std::borrow::Cow; - -use super::{Theme, Window}; +use super::{Config, Theme, Window}; use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; use crate::Action; -/// An optionally-owning (boxed) reference -/// -/// This is related but not identical to [`Cow`]. -pub enum MaybeBoxed<'a, B: 'a + ?Sized> { - Borrowed(&'a B), - Boxed(Box), -} - -impl AsRef for MaybeBoxed<'_, T> { - fn as_ref(&self) -> &T { - match self { - MaybeBoxed::Borrowed(r) => r, - MaybeBoxed::Boxed(b) => b.as_ref(), - } - } -} - /// As [`Theme`], but without associated types /// /// This trait is implemented automatically for all implementations of @@ -38,10 +18,10 @@ impl AsRef for MaybeBoxed<'_, T> { /// trait is required. pub trait ThemeDst: ThemeControl { /// Get current configuration - fn config(&self) -> MaybeBoxed; + fn config(&self) -> std::borrow::Cow; /// Apply/set the passed config - fn apply_config(&mut self, config: &dyn Any) -> Action; + fn apply_config(&mut self, config: &Config) -> Action; /// Theme initialisation /// @@ -72,15 +52,12 @@ pub trait ThemeDst: ThemeControl { } impl> ThemeDst for T { - fn config(&self) -> MaybeBoxed { - match self.config() { - Cow::Borrowed(config) => MaybeBoxed::Borrowed(config), - Cow::Owned(config) => MaybeBoxed::Boxed(Box::new(config)), - } + fn config(&self) -> std::borrow::Cow { + self.config() } - fn apply_config(&mut self, config: &dyn Any) -> Action { - self.apply_config(config.downcast_ref().unwrap()) + fn apply_config(&mut self, config: &Config) -> Action { + self.apply_config(config) } fn init(&mut self, shared: &mut SharedState) { diff --git a/crates/kas-core/src/theme/traits.rs b/crates/kas-core/src/theme/traits.rs index 7d7f5e676..5d031e930 100644 --- a/crates/kas-core/src/theme/traits.rs +++ b/crates/kas-core/src/theme/traits.rs @@ -5,7 +5,7 @@ //! Theme traits -use super::{ColorsLinear, ColorsSrgb, RasterConfig, ThemeDraw, ThemeSize}; +use super::{ColorsLinear, ColorsSrgb, Config, RasterConfig, ThemeDraw, ThemeSize}; use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; use crate::event::EventState; use crate::{autoimpl, Action}; @@ -99,9 +99,6 @@ pub trait ThemeConfig: /// large resources (e.g. fonts and icons) consider using external storage. #[autoimpl(for Box)] pub trait Theme: ThemeControl { - /// The associated config type - type Config: ThemeConfig; - /// The associated [`Window`] implementation. type Window: Window; @@ -112,10 +109,10 @@ pub trait Theme: ThemeControl { Self: 'a; /// Get current configuration - fn config(&self) -> std::borrow::Cow; + fn config(&self) -> std::borrow::Cow; /// Apply/set the passed config - fn apply_config(&mut self, config: &Self::Config) -> Action; + fn apply_config(&mut self, config: &Config) -> Action; /// Theme initialisation /// diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index cf3886b56..d82028842 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -92,16 +92,14 @@ impl Theme for ShadedTheme where DS::Draw: DrawRoundedImpl + DrawShadedImpl, { - type Config = Config; type Window = dim::Window; - type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { >::config(&self.base) } - fn apply_config(&mut self, config: &Self::Config) -> Action { + fn apply_config(&mut self, config: &Config) -> Action { >::apply_config(&mut self.base, config) } From e0a2744899de66e6e1640b9c5eb5704baa5de9ef Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:18:37 +0100 Subject: [PATCH 03/20] Prefer 'use crate::' over 'use kas::' in kas-core --- crates/kas-core/src/app/event_loop.rs | 4 ++-- crates/kas-core/src/app/shared.rs | 10 +++++----- crates/kas-core/src/app/window.rs | 15 +++++++-------- crates/kas-core/src/decorations.rs | 4 ++-- crates/kas-core/src/text/mod.rs | 2 +- crates/kas-core/src/theme/flat_theme.rs | 22 +++++++++++----------- crates/kas-core/src/theme/simple_theme.rs | 22 +++++++++++----------- 7 files changed, 39 insertions(+), 40 deletions(-) diff --git a/crates/kas-core/src/app/event_loop.rs b/crates/kas-core/src/app/event_loop.rs index c76dea882..2f0694a93 100644 --- a/crates/kas-core/src/app/event_loop.rs +++ b/crates/kas-core/src/app/event_loop.rs @@ -7,8 +7,8 @@ use super::{AppData, AppGraphicsBuilder, AppState, Pending}; use super::{ProxyAction, Window}; -use kas::theme::Theme; -use kas::{Action, WindowId}; +use crate::theme::Theme; +use crate::{Action, WindowId}; use std::collections::HashMap; use std::time::Instant; use winit::event::{Event, StartCause}; diff --git a/crates/kas-core/src/app/shared.rs b/crates/kas-core/src/app/shared.rs index b144437f2..95d3bf4de 100644 --- a/crates/kas-core/src/app/shared.rs +++ b/crates/kas-core/src/app/shared.rs @@ -6,11 +6,11 @@ //! Shared state use super::{AppData, AppGraphicsBuilder, Error, Pending, Platform}; -use kas::config::Options; -use kas::draw::DrawShared; -use kas::theme::{Theme, ThemeControl}; -use kas::util::warn_about_error; -use kas::{draw, messages::MessageStack, Action, WindowId}; +use crate::config::Options; +use crate::draw::DrawShared; +use crate::theme::{Theme, ThemeControl}; +use crate::util::warn_about_error; +use crate::{draw, messages::MessageStack, Action, WindowId}; use std::any::TypeId; use std::cell::RefCell; use std::collections::VecDeque; diff --git a/crates/kas-core/src/app/window.rs b/crates/kas-core/src/app/window.rs index 81b9db5eb..eb235a046 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -8,14 +8,13 @@ use super::common::WindowSurface; use super::shared::{AppSharedState, AppState}; use super::{AppData, AppGraphicsBuilder, ProxyAction}; -use kas::cast::{Cast, Conv}; -use kas::draw::{color::Rgba, AnimationState, DrawSharedImpl}; -use kas::event::{config::WindowConfig, ConfigCx, CursorIcon, EventState}; -use kas::geom::{Coord, Rect, Size}; -use kas::layout::SolveCache; -use kas::theme::{DrawCx, SizeCx, ThemeSize}; -use kas::theme::{Theme, Window as _}; -use kas::{autoimpl, messages::MessageStack, Action, Id, Layout, LayoutExt, Widget, WindowId}; +use crate::cast::{Cast, Conv}; +use crate::draw::{color::Rgba, AnimationState, DrawSharedImpl}; +use crate::event::{config::WindowConfig, ConfigCx, CursorIcon, EventState}; +use crate::geom::{Coord, Rect, Size}; +use crate::layout::SolveCache; +use crate::theme::{DrawCx, SizeCx, Theme, ThemeSize, Window as _}; +use crate::{autoimpl, messages::MessageStack, Action, Id, Layout, LayoutExt, Widget, WindowId}; use std::mem::take; use std::sync::Arc; use std::time::{Duration, Instant}; diff --git a/crates/kas-core/src/decorations.rs b/crates/kas-core/src/decorations.rs index fdcd4cd3d..8c218a90a 100644 --- a/crates/kas-core/src/decorations.rs +++ b/crates/kas-core/src/decorations.rs @@ -8,9 +8,9 @@ //! Note: due to definition in kas-core, some widgets must be duplicated. use crate::event::{CursorIcon, ResizeDirection}; +use crate::prelude::*; +use crate::theme::MarkStyle; use crate::theme::{Text, TextClass}; -use kas::prelude::*; -use kas::theme::MarkStyle; use kas_macros::impl_scope; use std::fmt::Debug; diff --git a/crates/kas-core/src/text/mod.rs b/crates/kas-core/src/text/mod.rs index 600d60797..446eb3217 100644 --- a/crates/kas-core/src/text/mod.rs +++ b/crates/kas-core/src/text/mod.rs @@ -13,7 +13,7 @@ //! //! [KAS Text]: https://github.com/kas-gui/kas-text/ -#[allow(unused)] use kas::{event::ConfigCx, Layout}; +#[allow(unused)] use crate::{event::ConfigCx, Layout}; pub use kas_text::*; diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index dd49cd7ba..4631e7be6 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -10,17 +10,17 @@ use std::ops::Range; use std::time::Instant; use super::SimpleTheme; -use kas::cast::traits::*; -use kas::dir::{Direction, Directional}; -use kas::draw::{color::Rgba, *}; -use kas::event::EventState; -use kas::geom::*; -use kas::text::TextDisplay; -use kas::theme::dimensions as dim; -use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; -use kas::theme::{ColorsLinear, Config, InputState, Theme}; -use kas::theme::{ThemeControl, ThemeDraw, ThemeSize}; -use kas::{Action, Id}; +use crate::cast::traits::*; +use crate::dir::{Direction, Directional}; +use crate::draw::{color::Rgba, *}; +use crate::event::EventState; +use crate::geom::*; +use crate::text::TextDisplay; +use crate::theme::dimensions as dim; +use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; +use crate::theme::{ColorsLinear, Config, InputState, Theme}; +use crate::theme::{ThemeControl, ThemeDraw, ThemeSize}; +use crate::{Action, Id}; // Used to ensure a rectangular background is inside a circular corner. // Also the maximum inner radius of circular borders to overlap with this rect. diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index dcf644172..97ceca9a0 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -11,17 +11,17 @@ use std::ops::Range; use std::rc::Rc; use std::time::Instant; -use kas::cast::traits::*; -use kas::dir::{Direction, Directional}; -use kas::draw::{color::Rgba, *}; -use kas::event::EventState; -use kas::geom::*; -use kas::text::{fonts, Effect, TextDisplay}; -use kas::theme::dimensions as dim; -use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; -use kas::theme::{ColorsLinear, Config, InputState, Theme}; -use kas::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; -use kas::{Action, Id}; +use crate::cast::traits::*; +use crate::dir::{Direction, Directional}; +use crate::draw::{color::Rgba, *}; +use crate::event::EventState; +use crate::geom::*; +use crate::text::{fonts, Effect, TextDisplay}; +use crate::theme::dimensions as dim; +use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; +use crate::theme::{ColorsLinear, Config, InputState, Theme}; +use crate::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; +use crate::{Action, Id}; /// A simple theme /// From e1083bbd235a508f5e3444c8f34d24cb11d26a2b Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 29 Apr 2024 11:25:23 +0100 Subject: [PATCH 04/20] Reorganise kas_core::config module --- crates/kas-core/src/config/format.rs | 196 ++++++++++++++++++ crates/kas-core/src/config/mod.rs | 15 ++ .../src/{config.rs => config/options.rs} | 193 +---------------- .../src/{event => }/config/shortcuts.rs | 0 crates/kas-core/src/event/config.rs | 4 +- crates/kas-core/src/event/events.rs | 2 +- 6 files changed, 216 insertions(+), 194 deletions(-) create mode 100644 crates/kas-core/src/config/format.rs create mode 100644 crates/kas-core/src/config/mod.rs rename crates/kas-core/src/{config.rs => config/options.rs} (53%) rename crates/kas-core/src/{event => }/config/shortcuts.rs (100%) diff --git a/crates/kas-core/src/config/format.rs b/crates/kas-core/src/config/format.rs new file mode 100644 index 000000000..9d2ad4f87 --- /dev/null +++ b/crates/kas-core/src/config/format.rs @@ -0,0 +1,196 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Configuration formats and read/write support + +#[cfg(feature = "serde")] +use serde::{de::DeserializeOwned, Serialize}; +use std::path::Path; +use thiserror::Error; + +/// Configuration read/write/format errors +#[derive(Error, Debug)] +pub enum Error { + #[cfg(feature = "yaml")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "yaml")))] + #[error("config (de)serialisation to YAML failed")] + Yaml(#[from] serde_yaml::Error), + + #[cfg(feature = "json")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "json")))] + #[error("config (de)serialisation to JSON failed")] + Json(#[from] serde_json::Error), + + #[cfg(feature = "ron")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "ron")))] + #[error("config serialisation to RON failed")] + Ron(#[from] ron::Error), + + #[cfg(feature = "ron")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "ron")))] + #[error("config deserialisation from RON failed")] + RonSpanned(#[from] ron::error::SpannedError), + + #[cfg(feature = "toml")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "toml")))] + #[error("config deserialisation from TOML failed")] + TomlDe(#[from] toml::de::Error), + + #[cfg(feature = "toml")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "toml")))] + #[error("config serialisation to TOML failed")] + TomlSer(#[from] toml::ser::Error), + + #[error("error reading / writing config file")] + IoError(#[from] std::io::Error), + + #[error("format not supported: {0}")] + UnsupportedFormat(Format), +} + +/// Configuration serialisation formats +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Error)] +pub enum Format { + /// Not specified: guess from the path + #[default] + #[error("no format")] + None, + + /// JavaScript Object Notation + #[error("JSON")] + Json, + + /// Tom's Obvious Minimal Language + #[error("TOML")] + Toml, + + /// YAML Ain't Markup Language + #[error("YAML")] + Yaml, + + /// Rusty Object Notation + #[error("RON")] + Ron, + + /// Error: unable to guess format + #[error("(unknown format)")] + Unknown, +} + +impl Format { + /// Guess format from the path name + /// + /// This does not open the file. + /// + /// Potentially fallible: on error, returns [`Format::Unknown`]. + /// This may be due to unrecognised file extension or due to the required + /// feature not being enabled. + pub fn guess_from_path(path: &Path) -> Format { + // use == since there is no OsStr literal + if let Some(ext) = path.extension() { + if ext == "json" { + Format::Json + } else if ext == "toml" { + Format::Toml + } else if ext == "yaml" { + Format::Yaml + } else if ext == "ron" { + Format::Ron + } else { + Format::Unknown + } + } else { + Format::Unknown + } + } + + /// Read from a path + #[cfg(feature = "serde")] + pub fn read_path(self, path: &Path) -> Result { + log::info!("read_path: path={}, format={:?}", path.display(), self); + match self { + #[cfg(feature = "json")] + Format::Json => { + let r = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(serde_json::from_reader(r)?) + } + #[cfg(feature = "yaml")] + Format::Yaml => { + let r = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(serde_yaml::from_reader(r)?) + } + #[cfg(feature = "ron")] + Format::Ron => { + let r = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(ron::de::from_reader(r)?) + } + #[cfg(feature = "toml")] + Format::Toml => { + let contents = std::fs::read_to_string(path)?; + Ok(toml::from_str(&contents)?) + } + _ => { + let _ = path; // squelch unused warning + Err(Error::UnsupportedFormat(self)) + } + } + } + + /// Write to a path + #[cfg(feature = "serde")] + pub fn write_path(self, path: &Path, value: &T) -> Result<(), Error> { + log::info!("write_path: path={}, format={:?}", path.display(), self); + // Note: we use to_string*, not to_writer*, since the latter may + // generate incomplete documents on failure. + match self { + #[cfg(feature = "json")] + Format::Json => { + let text = serde_json::to_string_pretty(value)?; + std::fs::write(path, &text)?; + Ok(()) + } + #[cfg(feature = "yaml")] + Format::Yaml => { + let text = serde_yaml::to_string(value)?; + std::fs::write(path, text)?; + Ok(()) + } + #[cfg(feature = "ron")] + Format::Ron => { + let pretty = ron::ser::PrettyConfig::default(); + let text = ron::ser::to_string_pretty(value, pretty)?; + std::fs::write(path, &text)?; + Ok(()) + } + #[cfg(feature = "toml")] + Format::Toml => { + let content = toml::to_string(value)?; + std::fs::write(path, &content)?; + Ok(()) + } + _ => { + let _ = (path, value); // squelch unused warnings + Err(Error::UnsupportedFormat(self)) + } + } + } + + /// Guess format and load from a path + #[cfg(feature = "serde")] + #[inline] + pub fn guess_and_read_path(path: &Path) -> Result { + let format = Self::guess_from_path(path); + format.read_path(path) + } + + /// Guess format and write to a path + #[cfg(feature = "serde")] + #[inline] + pub fn guess_and_write_path(path: &Path, value: &T) -> Result<(), Error> { + let format = Self::guess_from_path(path); + format.write_path(path, value) + } +} diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs new file mode 100644 index 000000000..e80863c8e --- /dev/null +++ b/crates/kas-core/src/config/mod.rs @@ -0,0 +1,15 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Configuration items and utilities + +mod format; +pub use format::{Error, Format}; + +mod options; +pub use options::{ConfigMode, Options}; + +mod shortcuts; +pub use shortcuts::Shortcuts; diff --git a/crates/kas-core/src/config.rs b/crates/kas-core/src/config/options.rs similarity index 53% rename from crates/kas-core/src/config.rs rename to crates/kas-core/src/config/options.rs index d8cb364e0..d8490c721 100644 --- a/crates/kas-core/src/config.rs +++ b/crates/kas-core/src/config/options.rs @@ -3,17 +3,15 @@ // You may obtain a copy of the License in the LICENSE-APACHE file or at: // https://www.apache.org/licenses/LICENSE-2.0 -//! Configuration read/write utilities +//! Configuration options +use super::Error; +#[cfg(feature = "serde")] use super::Format; use crate::draw::DrawSharedImpl; use crate::theme::{Theme, ThemeConfig}; #[cfg(feature = "serde")] use crate::util::warn_about_error; -#[cfg(feature = "serde")] -use serde::{de::DeserializeOwned, Serialize}; use std::env::var; -use std::path::Path; use std::path::PathBuf; -use thiserror::Error; /// Config mode /// @@ -32,191 +30,6 @@ pub enum ConfigMode { WriteDefault, } -/// Configuration read/write/format errors -#[derive(Error, Debug)] -pub enum Error { - #[cfg(feature = "yaml")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "yaml")))] - #[error("config (de)serialisation to YAML failed")] - Yaml(#[from] serde_yaml::Error), - - #[cfg(feature = "json")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "json")))] - #[error("config (de)serialisation to JSON failed")] - Json(#[from] serde_json::Error), - - #[cfg(feature = "ron")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "ron")))] - #[error("config serialisation to RON failed")] - Ron(#[from] ron::Error), - - #[cfg(feature = "ron")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "ron")))] - #[error("config deserialisation from RON failed")] - RonSpanned(#[from] ron::error::SpannedError), - - #[cfg(feature = "toml")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "toml")))] - #[error("config deserialisation from TOML failed")] - TomlDe(#[from] toml::de::Error), - - #[cfg(feature = "toml")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "toml")))] - #[error("config serialisation to TOML failed")] - TomlSer(#[from] toml::ser::Error), - - #[error("error reading / writing config file")] - IoError(#[from] std::io::Error), - - #[error("format not supported: {0}")] - UnsupportedFormat(Format), -} - -/// Configuration serialisation formats -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Error)] -pub enum Format { - /// Not specified: guess from the path - #[default] - #[error("no format")] - None, - - /// JavaScript Object Notation - #[error("JSON")] - Json, - - /// Tom's Obvious Minimal Language - #[error("TOML")] - Toml, - - /// YAML Ain't Markup Language - #[error("YAML")] - Yaml, - - /// Rusty Object Notation - #[error("RON")] - Ron, - - /// Error: unable to guess format - #[error("(unknown format)")] - Unknown, -} - -impl Format { - /// Guess format from the path name - /// - /// This does not open the file. - /// - /// Potentially fallible: on error, returns [`Format::Unknown`]. - /// This may be due to unrecognised file extension or due to the required - /// feature not being enabled. - pub fn guess_from_path(path: &Path) -> Format { - // use == since there is no OsStr literal - if let Some(ext) = path.extension() { - if ext == "json" { - Format::Json - } else if ext == "toml" { - Format::Toml - } else if ext == "yaml" { - Format::Yaml - } else if ext == "ron" { - Format::Ron - } else { - Format::Unknown - } - } else { - Format::Unknown - } - } - - /// Read from a path - #[cfg(feature = "serde")] - pub fn read_path(self, path: &Path) -> Result { - log::info!("read_path: path={}, format={:?}", path.display(), self); - match self { - #[cfg(feature = "json")] - Format::Json => { - let r = std::io::BufReader::new(std::fs::File::open(path)?); - Ok(serde_json::from_reader(r)?) - } - #[cfg(feature = "yaml")] - Format::Yaml => { - let r = std::io::BufReader::new(std::fs::File::open(path)?); - Ok(serde_yaml::from_reader(r)?) - } - #[cfg(feature = "ron")] - Format::Ron => { - let r = std::io::BufReader::new(std::fs::File::open(path)?); - Ok(ron::de::from_reader(r)?) - } - #[cfg(feature = "toml")] - Format::Toml => { - let contents = std::fs::read_to_string(path)?; - Ok(toml::from_str(&contents)?) - } - _ => { - let _ = path; // squelch unused warning - Err(Error::UnsupportedFormat(self)) - } - } - } - - /// Write to a path - #[cfg(feature = "serde")] - pub fn write_path(self, path: &Path, value: &T) -> Result<(), Error> { - log::info!("write_path: path={}, format={:?}", path.display(), self); - // Note: we use to_string*, not to_writer*, since the latter may - // generate incomplete documents on failure. - match self { - #[cfg(feature = "json")] - Format::Json => { - let text = serde_json::to_string_pretty(value)?; - std::fs::write(path, &text)?; - Ok(()) - } - #[cfg(feature = "yaml")] - Format::Yaml => { - let text = serde_yaml::to_string(value)?; - std::fs::write(path, text)?; - Ok(()) - } - #[cfg(feature = "ron")] - Format::Ron => { - let pretty = ron::ser::PrettyConfig::default(); - let text = ron::ser::to_string_pretty(value, pretty)?; - std::fs::write(path, &text)?; - Ok(()) - } - #[cfg(feature = "toml")] - Format::Toml => { - let content = toml::to_string(value)?; - std::fs::write(path, &content)?; - Ok(()) - } - _ => { - let _ = (path, value); // squelch unused warnings - Err(Error::UnsupportedFormat(self)) - } - } - } - - /// Guess format and load from a path - #[cfg(feature = "serde")] - #[inline] - pub fn guess_and_read_path(path: &Path) -> Result { - let format = Self::guess_from_path(path); - format.read_path(path) - } - - /// Guess format and write to a path - #[cfg(feature = "serde")] - #[inline] - pub fn guess_and_write_path(path: &Path, value: &T) -> Result<(), Error> { - let format = Self::guess_from_path(path); - format.write_path(path, value) - } -} - /// Application configuration options #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Options { diff --git a/crates/kas-core/src/event/config/shortcuts.rs b/crates/kas-core/src/config/shortcuts.rs similarity index 100% rename from crates/kas-core/src/event/config/shortcuts.rs rename to crates/kas-core/src/config/shortcuts.rs diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/event/config.rs index fc2d19288..551dde511 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/event/config.rs @@ -5,11 +5,9 @@ //! Event handling configuration -mod shortcuts; -pub use shortcuts::Shortcuts; - use super::ModifiersState; use crate::cast::{Cast, CastFloat}; +use crate::config::Shortcuts; use crate::geom::Offset; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; diff --git a/crates/kas-core/src/event/events.rs b/crates/kas-core/src/event/events.rs index f2ab23d32..f9bb7599f 100644 --- a/crates/kas-core/src/event/events.rs +++ b/crates/kas-core/src/event/events.rs @@ -351,7 +351,7 @@ impl Event { /// `Command` events are mostly produced as a result of OS-specific keyboard /// bindings; for example, [`Command::Copy`] is produced by pressing /// Command+C on MacOS or Ctrl+C on other platforms. -/// See [`crate::event::config::Shortcuts`] for more on these bindings. +/// See [`crate::config::Shortcuts`] for more on these bindings. /// /// A `Command` event does not necessarily come from keyboard input; for example /// some menu widgets send [`Command::Activate`] to trigger an entry as a result From 0ccc8b6b661abdfd79b26325ea58b08431d3195e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:18:54 +0100 Subject: [PATCH 05/20] Move kas_core::event::config -> kas_core::config::event --- crates/kas-core/src/app/app.rs | 3 +-- crates/kas-core/src/app/shared.rs | 6 +++--- crates/kas-core/src/app/window.rs | 3 ++- crates/kas-core/src/{event/config.rs => config/event.rs} | 2 +- crates/kas-core/src/config/mod.rs | 2 ++ crates/kas-core/src/config/options.rs | 8 ++++---- crates/kas-core/src/event/cx/cx_pub.rs | 2 +- crates/kas-core/src/event/cx/mod.rs | 2 +- crates/kas-core/src/event/mod.rs | 2 -- crates/kas-widgets/src/event_config.rs | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) rename crates/kas-core/src/{event/config.rs => config/event.rs} (99%) diff --git a/crates/kas-core/src/app/app.rs b/crates/kas-core/src/app/app.rs index eed45f472..7c08ddd2d 100644 --- a/crates/kas-core/src/app/app.rs +++ b/crates/kas-core/src/app/app.rs @@ -6,9 +6,8 @@ //! [`Application`] and supporting elements use super::{AppData, AppGraphicsBuilder, AppState, Platform, ProxyAction, Result}; -use crate::config::Options; +use crate::config::{event, Options}; use crate::draw::{DrawShared, DrawSharedImpl}; -use crate::event; use crate::theme::{self, Theme, ThemeConfig}; use crate::util::warn_about_error; use crate::{impl_scope, Window, WindowId}; diff --git a/crates/kas-core/src/app/shared.rs b/crates/kas-core/src/app/shared.rs index 95d3bf4de..dc677d01a 100644 --- a/crates/kas-core/src/app/shared.rs +++ b/crates/kas-core/src/app/shared.rs @@ -6,7 +6,7 @@ //! Shared state use super::{AppData, AppGraphicsBuilder, Error, Pending, Platform}; -use crate::config::Options; +use crate::config::{event, Options}; use crate::draw::DrawShared; use crate::theme::{Theme, ThemeControl}; use crate::util::warn_about_error; @@ -23,7 +23,7 @@ use std::task::Waker; /// Application state used by [`AppShared`] pub(crate) struct AppSharedState> { pub(super) platform: Platform, - pub(super) config: Rc>, + pub(super) config: Rc>, #[cfg(feature = "clipboard")] clipboard: Option, pub(super) draw: draw::SharedState, @@ -52,7 +52,7 @@ where draw_shared: G::Shared, mut theme: T, options: Options, - config: Rc>, + config: Rc>, ) -> Result { let platform = pw.platform(); let mut draw = kas::draw::SharedState::new(draw_shared); diff --git a/crates/kas-core/src/app/window.rs b/crates/kas-core/src/app/window.rs index eb235a046..7e8194b2c 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -9,8 +9,9 @@ use super::common::WindowSurface; use super::shared::{AppSharedState, AppState}; use super::{AppData, AppGraphicsBuilder, ProxyAction}; use crate::cast::{Cast, Conv}; +use crate::config::event::WindowConfig; use crate::draw::{color::Rgba, AnimationState, DrawSharedImpl}; -use crate::event::{config::WindowConfig, ConfigCx, CursorIcon, EventState}; +use crate::event::{ConfigCx, CursorIcon, EventState}; use crate::geom::{Coord, Rect, Size}; use crate::layout::SolveCache; use crate::theme::{DrawCx, SizeCx, Theme, ThemeSize, Window as _}; diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/config/event.rs similarity index 99% rename from crates/kas-core/src/event/config.rs rename to crates/kas-core/src/config/event.rs index 551dde511..189b2afc0 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/config/event.rs @@ -5,9 +5,9 @@ //! Event handling configuration -use super::ModifiersState; use crate::cast::{Cast, CastFloat}; use crate::config::Shortcuts; +use crate::event::ModifiersState; use crate::geom::Offset; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index e80863c8e..64b42383e 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -5,6 +5,8 @@ //! Configuration items and utilities +pub mod event; + mod format; pub use format::{Error, Format}; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs index d8490c721..685f9628e 100644 --- a/crates/kas-core/src/config/options.rs +++ b/crates/kas-core/src/config/options.rs @@ -5,8 +5,8 @@ //! Configuration options -use super::Error; #[cfg(feature = "serde")] use super::Format; +use super::{event, Error}; use crate::draw::DrawSharedImpl; use crate::theme::{Theme, ThemeConfig}; #[cfg(feature = "serde")] use crate::util::warn_about_error; @@ -145,7 +145,7 @@ impl Options { /// Load/save KAS config on start /// /// Requires feature "serde" to load/save config. - pub fn read_config(&self) -> Result { + pub fn read_config(&self) -> Result { #[cfg(feature = "serde")] if !self.config_path.as_os_str().is_empty() { return match self.config_mode { @@ -155,7 +155,7 @@ impl Options { } #[cfg(feature = "serde")] ConfigMode::WriteDefault => { - let config: kas::event::Config = Default::default(); + let config: event::Config = Default::default(); if let Err(error) = Format::guess_and_write_path(&self.config_path, &config) { warn_about_error("failed to write default config: ", &error); } @@ -172,7 +172,7 @@ impl Options { /// Requires feature "serde" to save config. pub fn write_config>( &self, - _config: &kas::event::Config, + _config: &event::Config, _theme: &T, ) -> Result<(), Error> { #[cfg(feature = "serde")] diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index 21ca6ff40..fe2a436cb 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -11,8 +11,8 @@ use std::time::Duration; use super::*; use crate::cast::Conv; +use crate::config::event::ChangeConfig; use crate::draw::DrawShared; -use crate::event::config::ChangeConfig; use crate::geom::{Offset, Vec2}; use crate::theme::{SizeCx, ThemeControl}; #[cfg(all(wayland_platform, feature = "clipboard"))] diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index 803dc120c..b3d57865f 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -16,10 +16,10 @@ use std::ops::{Deref, DerefMut}; use std::pin::Pin; use std::time::Instant; -use super::config::WindowConfig; use super::*; use crate::app::{AppShared, Platform, WindowDataErased}; use crate::cast::Cast; +use crate::config::event::WindowConfig; use crate::geom::Coord; use crate::messages::{Erased, MessageStack}; use crate::util::WidgetHierarchy; diff --git a/crates/kas-core/src/event/mod.rs b/crates/kas-core/src/event/mod.rs index ef55edeb8..cf508624d 100644 --- a/crates/kas-core/src/event/mod.rs +++ b/crates/kas-core/src/event/mod.rs @@ -58,7 +58,6 @@ //! [`Id`]: crate::Id pub mod components; -pub mod config; mod cx; #[cfg(not(winit))] mod enums; mod events; @@ -73,7 +72,6 @@ pub use winit::keyboard::{Key, ModifiersState, NamedKey, PhysicalKey}; pub use winit::window::{CursorIcon, ResizeDirection}; // used by Key #[allow(unused)] use crate::{Events, Widget}; -#[doc(no_inline)] pub use config::Config; pub use cx::*; #[cfg(not(winit))] pub use enums::*; pub use events::*; diff --git a/crates/kas-widgets/src/event_config.rs b/crates/kas-widgets/src/event_config.rs index e5333b4d9..4d296bf38 100644 --- a/crates/kas-widgets/src/event_config.rs +++ b/crates/kas-widgets/src/event_config.rs @@ -6,7 +6,7 @@ //! Drivers for configuration types use crate::{Button, CheckButton, ComboBox, Spinner}; -use kas::event::config::{ChangeConfig, MousePan}; +use kas::config::event::{ChangeConfig, MousePan}; use kas::prelude::*; impl_scope! { From 1a7affb42264cb9c90d464fdd8594e4de7171d88 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 29 Apr 2024 13:48:29 +0100 Subject: [PATCH 06/20] Let fn WindowConfig::shortcuts return Ref --- crates/kas-core/src/config/event.rs | 5 ++--- crates/kas-core/src/event/cx/mod.rs | 4 +--- crates/kas-widgets/src/edit.rs | 5 ++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/kas-core/src/config/event.rs b/crates/kas-core/src/config/event.rs index 189b2afc0..6ea5f689d 100644 --- a/crates/kas-core/src/config/event.rs +++ b/crates/kas-core/src/config/event.rs @@ -274,9 +274,8 @@ impl WindowConfig { } /// Access shortcut config - pub fn shortcuts T, T>(&self, f: F) -> T { - let base = self.config.borrow(); - f(&base.shortcuts) + pub fn shortcuts(&self) -> Ref { + Ref::map(self.config.borrow(), |c| &c.shortcuts) } /// Minimum frame time diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index b3d57865f..2fb7ae1ae 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -395,9 +395,7 @@ impl<'a> EventCx<'a> { widget.id() ); - let opt_command = self - .config - .shortcuts(|s| s.try_match(self.modifiers, &vkey)); + let opt_command = self.config.shortcuts().try_match(self.modifiers, &vkey); if let Some(cmd) = opt_command { let mut targets = vec![]; diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index 423bda02c..19a25ad82 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -803,7 +803,10 @@ impl_scope! { if let Some(text) = event.text { self.received_text(cx, data, &text) } else { - if let Some(cmd) = cx.config().shortcuts(|s| s.try_match(cx.modifiers(), &event.logical_key)) { + let opt_cmd = cx.config() + .shortcuts() + .try_match(cx.modifiers(), &event.logical_key); + if let Some(cmd) = opt_cmd { match self.control_key(cx, data, cmd, Some(event.physical_key)) { Ok(r) => r, Err(NotReady) => Used, From 977568319dbb927cc461192668d8b824a4440335 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 29 Apr 2024 14:02:09 +0100 Subject: [PATCH 07/20] Add WindowConfig::change_config --- crates/kas-core/src/config/event.rs | 16 ++++++++++++++++ crates/kas-core/src/event/cx/cx_pub.rs | 8 +------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/kas-core/src/config/event.rs b/crates/kas-core/src/config/event.rs index 6ea5f689d..ea7c96017 100644 --- a/crates/kas-core/src/config/event.rs +++ b/crates/kas-core/src/config/event.rs @@ -9,6 +9,7 @@ use crate::cast::{Cast, CastFloat}; use crate::config::Shortcuts; use crate::event::ModifiersState; use crate::geom::Offset; +use crate::Action; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::cell::{Ref, RefCell}; @@ -188,6 +189,21 @@ impl WindowConfig { pub fn borrow(&self) -> Ref { self.config.borrow() } + + /// Update event configuration + #[inline] + pub fn change_config(&mut self, msg: ChangeConfig) -> Action { + match self.config.try_borrow_mut() { + Ok(mut config) => { + config.change_config(msg); + Action::EVENT_CONFIG + } + Err(_) => { + log::error!("WindowConfig::change_config: failed to mutably borrow config"); + Action::empty() + } + } + } } impl WindowConfig { diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index fe2a436cb..456f1fc2d 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -158,13 +158,7 @@ impl EventState { /// Update event configuration #[inline] pub fn change_config(&mut self, msg: ChangeConfig) { - match self.config.config.try_borrow_mut() { - Ok(mut config) => { - config.change_config(msg); - self.action |= Action::EVENT_CONFIG; - } - Err(_) => log::error!("EventState::change_config: failed to mutably borrow config"), - } + self.action |= self.config.change_config(msg); } /// Set/unset a widget as disabled From aaad9385fe01b560408d0bbf707a65de244dfe69 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 29 Apr 2024 14:33:17 +0100 Subject: [PATCH 08/20] Add kas_core::config::Config, WindowConfig event::Config is now a field of the above Config --- crates/kas-core/src/app/app.rs | 12 +- crates/kas-core/src/app/shared.rs | 6 +- crates/kas-core/src/app/window.rs | 2 +- crates/kas-core/src/config/config.rs | 150 +++++++++++++++++++++++ crates/kas-core/src/config/event.rs | 136 ++++---------------- crates/kas-core/src/config/mod.rs | 3 + crates/kas-core/src/config/options.rs | 8 +- crates/kas-core/src/event/components.rs | 22 ++-- crates/kas-core/src/event/cx/cx_pub.rs | 10 +- crates/kas-core/src/event/cx/mod.rs | 2 +- crates/kas-core/src/event/cx/platform.rs | 4 +- crates/kas-view/src/list_view.rs | 4 +- crates/kas-view/src/matrix_view.rs | 4 +- crates/kas-widgets/src/edit.rs | 2 +- crates/kas-widgets/src/event_config.rs | 22 ++-- crates/kas-widgets/src/menu/menubar.rs | 2 +- crates/kas-widgets/src/scroll_bar.rs | 2 +- crates/kas-widgets/src/scroll_label.rs | 2 +- crates/kas-widgets/src/scroll_text.rs | 2 +- 19 files changed, 233 insertions(+), 162 deletions(-) create mode 100644 crates/kas-core/src/config/config.rs diff --git a/crates/kas-core/src/app/app.rs b/crates/kas-core/src/app/app.rs index 7c08ddd2d..3b6222d73 100644 --- a/crates/kas-core/src/app/app.rs +++ b/crates/kas-core/src/app/app.rs @@ -6,7 +6,7 @@ //! [`Application`] and supporting elements use super::{AppData, AppGraphicsBuilder, AppState, Platform, ProxyAction, Result}; -use crate::config::{event, Options}; +use crate::config::{Config, Options}; use crate::draw::{DrawShared, DrawSharedImpl}; use crate::theme::{self, Theme, ThemeConfig}; use crate::util::warn_about_error; @@ -26,7 +26,7 @@ impl_scope! { graphical: G, theme: T, options: Option, - config: Option>>, + config: Option>>, } impl Self { @@ -53,19 +53,19 @@ impl_scope! { /// Use the specified event `config` /// - /// This is a wrapper around [`Self::with_event_config_rc`]. + /// This is a wrapper around [`Self::with_config_rc`]. /// /// If omitted, config is provided by [`Options::read_config`]. #[inline] - pub fn with_event_config(self, config: event::Config) -> Self { - self.with_event_config_rc(Rc::new(RefCell::new(config))) + pub fn with_config(self, config: Config) -> Self { + self.with_config_rc(Rc::new(RefCell::new(config))) } /// Use the specified event `config` /// /// If omitted, config is provided by [`Options::read_config`]. #[inline] - pub fn with_event_config_rc(mut self, config: Rc>) -> Self { + pub fn with_config_rc(mut self, config: Rc>) -> Self { self.config = Some(config); self } diff --git a/crates/kas-core/src/app/shared.rs b/crates/kas-core/src/app/shared.rs index dc677d01a..e81d7ff1d 100644 --- a/crates/kas-core/src/app/shared.rs +++ b/crates/kas-core/src/app/shared.rs @@ -6,7 +6,7 @@ //! Shared state use super::{AppData, AppGraphicsBuilder, Error, Pending, Platform}; -use crate::config::{event, Options}; +use crate::config::{Config, Options}; use crate::draw::DrawShared; use crate::theme::{Theme, ThemeControl}; use crate::util::warn_about_error; @@ -23,7 +23,7 @@ use std::task::Waker; /// Application state used by [`AppShared`] pub(crate) struct AppSharedState> { pub(super) platform: Platform, - pub(super) config: Rc>, + pub(super) config: Rc>, #[cfg(feature = "clipboard")] clipboard: Option, pub(super) draw: draw::SharedState, @@ -52,7 +52,7 @@ where draw_shared: G::Shared, mut theme: T, options: Options, - config: Rc>, + config: Rc>, ) -> Result { let platform = pw.platform(); let mut draw = kas::draw::SharedState::new(draw_shared); diff --git a/crates/kas-core/src/app/window.rs b/crates/kas-core/src/app/window.rs index 7e8194b2c..33b907ec1 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -9,7 +9,7 @@ use super::common::WindowSurface; use super::shared::{AppSharedState, AppState}; use super::{AppData, AppGraphicsBuilder, ProxyAction}; use crate::cast::{Cast, Conv}; -use crate::config::event::WindowConfig; +use crate::config::WindowConfig; use crate::draw::{color::Rgba, AnimationState, DrawSharedImpl}; use crate::event::{ConfigCx, CursorIcon, EventState}; use crate::geom::{Coord, Rect, Size}; diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs new file mode 100644 index 000000000..b9ec2a1aa --- /dev/null +++ b/crates/kas-core/src/config/config.rs @@ -0,0 +1,150 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Top-level configuration struct + +use super::event::{self, ChangeConfig}; +use crate::cast::Cast; +use crate::config::Shortcuts; +use crate::Action; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::cell::{Ref, RefCell}; +use std::rc::Rc; +use std::time::Duration; + +/// Base configuration +/// +/// This is serializable (using `feature = "serde"`) with the following fields: +/// +/// > `event`: [`event::Config`] \ +/// > `shortcuts`: [`Shortcuts`] +/// +/// For descriptions of configuration effects, see [`WindowConfig`] methods. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Config { + pub event: event::Config, + + #[cfg_attr(feature = "serde", serde(default = "Shortcuts::platform_defaults"))] + pub shortcuts: Shortcuts, + + #[cfg_attr(feature = "serde", serde(default = "defaults::frame_dur_nanos"))] + frame_dur_nanos: u32, + + #[cfg_attr(feature = "serde", serde(skip))] + is_dirty: bool, +} + +impl Default for Config { + fn default() -> Self { + Config { + event: event::Config::default(), + shortcuts: Shortcuts::platform_defaults(), + frame_dur_nanos: defaults::frame_dur_nanos(), + is_dirty: false, + } + } +} + +impl Config { + /// Has the config ever been updated? + #[inline] + pub fn is_dirty(&self) -> bool { + self.is_dirty + } + + pub(crate) fn change_config(&mut self, msg: event::ChangeConfig) { + self.event.change_config(msg); + self.is_dirty = true; + } +} + +/// Configuration, adapted for the application and window scale +#[derive(Clone, Debug)] +pub struct WindowConfig { + pub(super) config: Rc>, + pub(super) scroll_flick_sub: f32, + pub(super) scroll_dist: f32, + pub(super) pan_dist_thresh: f32, + /// Whether navigation focus is enabled for this application window + pub(crate) nav_focus: bool, + pub(super) frame_dur: Duration, +} + +impl WindowConfig { + /// Construct + /// + /// It is required to call [`Self::update`] before usage. + #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] + #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] + pub fn new(config: Rc>) -> Self { + WindowConfig { + config, + scroll_flick_sub: f32::NAN, + scroll_dist: f32::NAN, + pan_dist_thresh: f32::NAN, + nav_focus: true, + frame_dur: Default::default(), + } + } + + /// Update window-specific/cached values + #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] + #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] + pub fn update(&mut self, scale_factor: f32, dpem: f32) { + let base = self.config.borrow(); + self.scroll_flick_sub = base.event.scroll_flick_sub * scale_factor; + self.scroll_dist = base.event.scroll_dist_em * dpem; + self.pan_dist_thresh = base.event.pan_dist_thresh * scale_factor; + self.frame_dur = Duration::from_nanos(base.frame_dur_nanos.cast()); + } + + /// Borrow access to the [`Config`] + pub fn borrow(&self) -> Ref { + self.config.borrow() + } + + /// Update event configuration + #[inline] + pub fn change_config(&mut self, msg: ChangeConfig) -> Action { + match self.config.try_borrow_mut() { + Ok(mut config) => { + config.change_config(msg); + Action::EVENT_CONFIG + } + Err(_) => { + log::error!("WindowConfig::change_config: failed to mutably borrow config"); + Action::empty() + } + } + } +} + +impl WindowConfig { + /// Access event config + pub fn event(&self) -> event::WindowConfig { + event::WindowConfig(self) + } + + /// Access shortcut config + pub fn shortcuts(&self) -> Ref { + Ref::map(self.config.borrow(), |c| &c.shortcuts) + } + + /// Minimum frame time + #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] + #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] + #[inline] + pub fn frame_dur(&self) -> Duration { + self.frame_dur + } +} + +mod defaults { + pub fn frame_dur_nanos() -> u32 { + 12_500_000 // 1e9 / 80 + } +} diff --git a/crates/kas-core/src/config/event.rs b/crates/kas-core/src/config/event.rs index ea7c96017..ee30e6653 100644 --- a/crates/kas-core/src/config/event.rs +++ b/crates/kas-core/src/config/event.rs @@ -6,14 +6,10 @@ //! Event handling configuration use crate::cast::{Cast, CastFloat}; -use crate::config::Shortcuts; use crate::event::ModifiersState; use crate::geom::Offset; -use crate::Action; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use std::cell::{Ref, RefCell}; -use std::rc::Rc; use std::time::Duration; /// Configuration message used to update [`Config`] @@ -47,8 +43,7 @@ pub enum ChangeConfig { /// > `mouse_pan`: [`MousePan`] \ /// > `mouse_text_pan`: [`MousePan`] \ /// > `mouse_nav_focus`: `bool` \ -/// > `touch_nav_focus`: `bool` \ -/// > `shortcuts`: [`Shortcuts`] +/// > `touch_nav_focus`: `bool` /// /// For descriptions of configuration effects, see [`WindowConfig`] methods. #[derive(Clone, Debug, PartialEq)] @@ -87,16 +82,6 @@ pub struct Config { pub mouse_nav_focus: bool, #[cfg_attr(feature = "serde", serde(default = "defaults::touch_nav_focus"))] pub touch_nav_focus: bool, - - // TODO: this is not "event" configuration; reorganise! - #[cfg_attr(feature = "serde", serde(default = "defaults::frame_dur_nanos"))] - frame_dur_nanos: u32, - - #[cfg_attr(feature = "serde", serde(default = "Shortcuts::platform_defaults"))] - pub shortcuts: Shortcuts, - - #[cfg_attr(feature = "serde", serde(skip))] - pub is_dirty: bool, } impl Default for Config { @@ -113,21 +98,12 @@ impl Default for Config { mouse_text_pan: defaults::mouse_text_pan(), mouse_nav_focus: defaults::mouse_nav_focus(), touch_nav_focus: defaults::touch_nav_focus(), - frame_dur_nanos: defaults::frame_dur_nanos(), - shortcuts: Shortcuts::platform_defaults(), - is_dirty: false, } } } impl Config { - /// Has the config ever been updated? - #[inline] - pub fn is_dirty(&self) -> bool { - self.is_dirty - } - - pub(crate) fn change_config(&mut self, msg: ChangeConfig) { + pub(super) fn change_config(&mut self, msg: ChangeConfig) { match msg { ChangeConfig::MenuDelay(v) => self.menu_delay_ms = v, ChangeConfig::TouchSelectDelay(v) => self.touch_select_delay_ms = v, @@ -142,81 +118,27 @@ impl Config { ChangeConfig::TouchNavFocus(v) => self.touch_nav_focus = v, ChangeConfig::ResetToDefault => *self = Config::default(), } - self.is_dirty = true; } } -/// Wrapper around [`Config`] to handle window-specific scaling +/// Accessor to event configuration +/// +/// This is a helper to read event configuration, adapted for the current +/// application and window scale. #[derive(Clone, Debug)] -pub struct WindowConfig { - pub(crate) config: Rc>, - scroll_flick_sub: f32, - scroll_dist: f32, - pan_dist_thresh: f32, - pub(crate) nav_focus: bool, - frame_dur: Duration, -} - -impl WindowConfig { - /// Construct - /// - /// It is required to call [`Self::update`] before usage. - #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] - #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn new(config: Rc>) -> Self { - WindowConfig { - config, - scroll_flick_sub: f32::NAN, - scroll_dist: f32::NAN, - pan_dist_thresh: f32::NAN, - nav_focus: true, - frame_dur: Default::default(), - } - } - - /// Update window-specific/cached values - #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] - #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn update(&mut self, scale_factor: f32, dpem: f32) { - let base = self.config.borrow(); - self.scroll_flick_sub = base.scroll_flick_sub * scale_factor; - self.scroll_dist = base.scroll_dist_em * dpem; - self.pan_dist_thresh = base.pan_dist_thresh * scale_factor; - self.frame_dur = Duration::from_nanos(base.frame_dur_nanos.cast()); - } +pub struct WindowConfig<'a>(pub(super) &'a super::WindowConfig); - /// Borrow access to the [`Config`] - pub fn borrow(&self) -> Ref { - self.config.borrow() - } - - /// Update event configuration - #[inline] - pub fn change_config(&mut self, msg: ChangeConfig) -> Action { - match self.config.try_borrow_mut() { - Ok(mut config) => { - config.change_config(msg); - Action::EVENT_CONFIG - } - Err(_) => { - log::error!("WindowConfig::change_config: failed to mutably borrow config"); - Action::empty() - } - } - } -} - -impl WindowConfig { +impl<'a> WindowConfig<'a> { /// Delay before opening/closing menus on mouse hover #[inline] pub fn menu_delay(&self) -> Duration { - Duration::from_millis(self.config.borrow().menu_delay_ms.cast()) + Duration::from_millis(self.0.borrow().event.menu_delay_ms.cast()) } /// Delay before switching from panning to (text) selection mode #[inline] pub fn touch_select_delay(&self) -> Duration { - Duration::from_millis(self.config.borrow().touch_select_delay_ms.cast()) + Duration::from_millis(self.0.borrow().event.touch_select_delay_ms.cast()) } /// Controls activation of glide/momentum scrolling @@ -226,7 +148,7 @@ impl WindowConfig { /// events within this time window are used to calculate the initial speed. #[inline] pub fn scroll_flick_timeout(&self) -> Duration { - Duration::from_millis(self.config.borrow().scroll_flick_timeout_ms.cast()) + Duration::from_millis(self.0.borrow().event.scroll_flick_timeout_ms.cast()) } /// Scroll flick velocity decay: `(mul, sub)` @@ -241,15 +163,18 @@ impl WindowConfig { /// Units are pixels/second (output is adjusted for the window's scale factor). #[inline] pub fn scroll_flick_decay(&self) -> (f32, f32) { - (self.config.borrow().scroll_flick_mul, self.scroll_flick_sub) + ( + self.0.borrow().event.scroll_flick_mul, + self.0.scroll_flick_sub, + ) } /// Get distance in pixels to scroll due to mouse wheel /// /// Calculates scroll distance from `(horiz, vert)` lines. pub fn scroll_distance(&self, lines: (f32, f32)) -> Offset { - let x = (self.scroll_dist * lines.0).cast_nearest(); - let y = (self.scroll_dist * lines.1).cast_nearest(); + let x = (self.0.scroll_dist * lines.0).cast_nearest(); + let y = (self.0.scroll_dist * lines.1).cast_nearest(); Offset(x, y) } @@ -262,44 +187,31 @@ impl WindowConfig { /// Units are pixels (output is adjusted for the window's scale factor). #[inline] pub fn pan_dist_thresh(&self) -> f32 { - self.pan_dist_thresh + self.0.pan_dist_thresh } /// When to pan general widgets (unhandled events) with the mouse #[inline] pub fn mouse_pan(&self) -> MousePan { - self.config.borrow().mouse_pan + self.0.borrow().event.mouse_pan } /// When to pan text fields with the mouse #[inline] pub fn mouse_text_pan(&self) -> MousePan { - self.config.borrow().mouse_text_pan + self.0.borrow().event.mouse_text_pan } /// Whether mouse clicks set keyboard navigation focus #[inline] pub fn mouse_nav_focus(&self) -> bool { - self.nav_focus && self.config.borrow().mouse_nav_focus + self.0.nav_focus && self.0.borrow().event.mouse_nav_focus } /// Whether touchscreen events set keyboard navigation focus #[inline] pub fn touch_nav_focus(&self) -> bool { - self.nav_focus && self.config.borrow().touch_nav_focus - } - - /// Access shortcut config - pub fn shortcuts(&self) -> Ref { - Ref::map(self.config.borrow(), |c| &c.shortcuts) - } - - /// Minimum frame time - #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] - #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - #[inline] - pub fn frame_dur(&self) -> Duration { - self.frame_dur + self.0.nav_focus && self.0.borrow().event.touch_nav_focus } } @@ -381,8 +293,4 @@ mod defaults { pub fn touch_nav_focus() -> bool { true } - - pub fn frame_dur_nanos() -> u32 { - 12_500_000 // 1e9 / 80 - } } diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index 64b42383e..bfb184acd 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -5,6 +5,9 @@ //! Configuration items and utilities +mod config; +pub use config::{Config, WindowConfig}; + pub mod event; mod format; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs index 685f9628e..864464bde 100644 --- a/crates/kas-core/src/config/options.rs +++ b/crates/kas-core/src/config/options.rs @@ -6,7 +6,7 @@ //! Configuration options #[cfg(feature = "serde")] use super::Format; -use super::{event, Error}; +use super::{Config, Error}; use crate::draw::DrawSharedImpl; use crate::theme::{Theme, ThemeConfig}; #[cfg(feature = "serde")] use crate::util::warn_about_error; @@ -145,7 +145,7 @@ impl Options { /// Load/save KAS config on start /// /// Requires feature "serde" to load/save config. - pub fn read_config(&self) -> Result { + pub fn read_config(&self) -> Result { #[cfg(feature = "serde")] if !self.config_path.as_os_str().is_empty() { return match self.config_mode { @@ -155,7 +155,7 @@ impl Options { } #[cfg(feature = "serde")] ConfigMode::WriteDefault => { - let config: event::Config = Default::default(); + let config: Config = Default::default(); if let Err(error) = Format::guess_and_write_path(&self.config_path, &config) { warn_about_error("failed to write default config: ", &error); } @@ -172,7 +172,7 @@ impl Options { /// Requires feature "serde" to save config. pub fn write_config>( &self, - _config: &event::Config, + _config: &Config, _theme: &T, ) -> Result<(), Error> { #[cfg(feature = "serde")] diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index d8fe2f2f7..8fb3f84e9 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -297,7 +297,7 @@ impl ScrollComponent { _ => return (false, Unused), }; let delta = match delta { - LineDelta(x, y) => cx.config().scroll_distance((x, y)), + LineDelta(x, y) => cx.config().event().scroll_distance((x, y)), PixelDelta(d) => d, }; self.offset - delta @@ -308,7 +308,7 @@ impl ScrollComponent { } Event::Scroll(delta) => { let delta = match delta { - LineDelta(x, y) => cx.config().scroll_distance((x, y)), + LineDelta(x, y) => cx.config().event().scroll_distance((x, y)), PixelDelta(d) => d, }; self.glide.stop(); @@ -333,16 +333,16 @@ impl ScrollComponent { Event::PressEnd { press, .. } if self.max_offset != Offset::ZERO && cx.config_enable_pan(*press) => { - let timeout = cx.config().scroll_flick_timeout(); - let pan_dist_thresh = cx.config().pan_dist_thresh(); + let timeout = cx.config().event().scroll_flick_timeout(); + let pan_dist_thresh = cx.config().event().pan_dist_thresh(); if self.glide.press_end(timeout, pan_dist_thresh) { cx.request_timer(id.clone(), TIMER_GLIDE, Duration::new(0, 0)); } } Event::Timer(pl) if pl == TIMER_GLIDE => { // Momentum/glide scrolling: update per arbitrary step time until movment stops. - let timeout = cx.config().scroll_flick_timeout(); - let decay = cx.config().scroll_flick_decay(); + let timeout = cx.config().event().scroll_flick_timeout(); + let decay = cx.config().event().scroll_flick_decay(); if let Some(delta) = self.glide.step(timeout, decay) { action = self.scroll_by_delta(cx, delta); @@ -425,7 +425,7 @@ impl TextInput { let icon = match *press { PressSource::Touch(touch_id) => { self.touch_phase = TouchPhase::Start(touch_id, press.coord); - let delay = cx.config().touch_select_delay(); + let delay = cx.config().event().touch_select_delay(); cx.request_timer(w_id.clone(), TIMER_SELECT, delay); None } @@ -477,8 +477,8 @@ impl TextInput { } } Event::PressEnd { press, .. } if press.is_primary() => { - let timeout = cx.config().scroll_flick_timeout(); - let pan_dist_thresh = cx.config().pan_dist_thresh(); + let timeout = cx.config().event().scroll_flick_timeout(); + let pan_dist_thresh = cx.config().event().pan_dist_thresh(); if self.glide.press_end(timeout, pan_dist_thresh) && (matches!(press.source, PressSource::Touch(id) if self.touch_phase == TouchPhase::Pan(id)) || matches!(press.source, PressSource::Mouse(..) if cx.config_enable_mouse_text_pan())) @@ -500,8 +500,8 @@ impl TextInput { }, Event::Timer(pl) if pl == TIMER_GLIDE => { // Momentum/glide scrolling: update per arbitrary step time until movment stops. - let timeout = cx.config().scroll_flick_timeout(); - let decay = cx.config().scroll_flick_decay(); + let timeout = cx.config().event().scroll_flick_timeout(); + let decay = cx.config().event().scroll_flick_decay(); if let Some(delta) = self.glide.step(timeout, decay) { let dur = Duration::from_millis(GLIDE_POLL_MS); cx.request_timer(w_id, TIMER_GLIDE, dur); diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index 456f1fc2d..5cefbc9b9 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -136,13 +136,19 @@ impl EventState { #[inline] pub fn config_enable_pan(&self, source: PressSource) -> bool { source.is_touch() - || source.is_primary() && self.config.mouse_pan().is_enabled_with(self.modifiers()) + || source.is_primary() + && self + .config + .event() + .mouse_pan() + .is_enabled_with(self.modifiers()) } /// Is mouse text panning enabled? #[inline] pub fn config_enable_mouse_text_pan(&self) -> bool { self.config + .event() .mouse_text_pan() .is_enabled_with(self.modifiers()) } @@ -152,7 +158,7 @@ impl EventState { /// Returns true when `dist` is large enough to switch to pan mode. #[inline] pub fn config_test_pan_thresh(&self, dist: Offset) -> bool { - Vec2::conv(dist).abs().max_comp() >= self.config.pan_dist_thresh() + Vec2::conv(dist).abs().max_comp() >= self.config.event().pan_dist_thresh() } /// Update event configuration diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index 2fb7ae1ae..a9dd7683f 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -19,7 +19,7 @@ use std::time::Instant; use super::*; use crate::app::{AppShared, Platform, WindowDataErased}; use crate::cast::Cast; -use crate::config::event::WindowConfig; +use crate::config::WindowConfig; use crate::geom::Coord; use crate::messages::{Erased, MessageStack}; use crate::util::WidgetHierarchy; diff --git a/crates/kas-core/src/event/cx/platform.rs b/crates/kas-core/src/event/cx/platform.rs index 308542dbe..83e2cf28f 100644 --- a/crates/kas-core/src/event/cx/platform.rs +++ b/crates/kas-core/src/event/cx/platform.rs @@ -474,7 +474,7 @@ impl<'a> EventCx<'a> { if state == ElementState::Pressed { if let Some(start_id) = self.hover.clone() { // No mouse grab but have a hover target - if self.config.mouse_nav_focus() { + if self.config.event().mouse_nav_focus() { if let Some(id) = self.nav_next(win.as_node(data), Some(&start_id), NavAdvance::None) { @@ -502,7 +502,7 @@ impl<'a> EventCx<'a> { TouchPhase::Started => { let start_id = win.find_id(data, coord); if let Some(id) = start_id.as_ref() { - if self.config.touch_nav_focus() { + if self.config.event().touch_nav_focus() { if let Some(id) = self.nav_next(win.as_node(data), Some(id), NavAdvance::None) { diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs index 6776ad010..ea598a896 100644 --- a/crates/kas-view/src/list_view.rs +++ b/crates/kas-view/src/list_view.rs @@ -673,7 +673,9 @@ impl_scope! { Unused }; } - Event::PressStart { ref press } if press.is_primary() && cx.config().mouse_nav_focus() => { + Event::PressStart { ref press } if + press.is_primary() && cx.config().event().mouse_nav_focus() => + { if let Some(index) = cx.last_child() { self.press_target = self.widgets[index].key.clone().map(|k| (index, k)); } diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs index 71a862732..02cc5e73e 100644 --- a/crates/kas-view/src/matrix_view.rs +++ b/crates/kas-view/src/matrix_view.rs @@ -637,7 +637,9 @@ impl_scope! { Unused }; } - Event::PressStart { ref press } if press.is_primary() && cx.config().mouse_nav_focus() => { + Event::PressStart { ref press } if + press.is_primary() && cx.config().event().mouse_nav_focus() => + { if let Some(index) = cx.last_child() { self.press_target = self.widgets[index].key.clone().map(|k| (index, k)); } diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index 19a25ad82..d07b6e262 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -818,7 +818,7 @@ impl_scope! { } Event::Scroll(delta) => { let delta2 = match delta { - ScrollDelta::LineDelta(x, y) => cx.config().scroll_distance((x, y)), + ScrollDelta::LineDelta(x, y) => cx.config().event().scroll_distance((x, y)), ScrollDelta::PixelDelta(coord) => coord, }; self.pan_delta(cx, delta2) diff --git a/crates/kas-widgets/src/event_config.rs b/crates/kas-widgets/src/event_config.rs index 4d296bf38..2bafe0558 100644 --- a/crates/kas-widgets/src/event_config.rs +++ b/crates/kas-widgets/src/event_config.rs @@ -105,45 +105,45 @@ impl_scope! { EventConfig { core: Default::default(), - menu_delay: Spinner::new(0..=5_000, |cx, _| cx.config().borrow().menu_delay_ms) + menu_delay: Spinner::new(0..=5_000, |cx, _| cx.config().borrow().event.menu_delay_ms) .with_step(50) .with_msg(ChangeConfig::MenuDelay), - touch_select_delay: Spinner::new(0..=5_000, |cx: &ConfigCx, _| cx.config().borrow().touch_select_delay_ms) + touch_select_delay: Spinner::new(0..=5_000, |cx: &ConfigCx, _| cx.config().borrow().event.touch_select_delay_ms) .with_step(50) .with_msg(ChangeConfig::TouchSelectDelay), - scroll_flick_timeout: Spinner::new(0..=500, |cx: &ConfigCx, _| cx.config().borrow().scroll_flick_timeout_ms) + scroll_flick_timeout: Spinner::new(0..=500, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_flick_timeout_ms) .with_step(5) .with_msg(ChangeConfig::ScrollFlickTimeout), - scroll_flick_mul: Spinner::new(0.0..=1.0, |cx: &ConfigCx, _| cx.config().borrow().scroll_flick_mul) + scroll_flick_mul: Spinner::new(0.0..=1.0, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_flick_mul) .with_step(0.0625) .with_msg(ChangeConfig::ScrollFlickMul), - scroll_flick_sub: Spinner::new(0.0..=1.0e4, |cx: &ConfigCx, _| cx.config().borrow().scroll_flick_sub) + scroll_flick_sub: Spinner::new(0.0..=1.0e4, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_flick_sub) .with_step(10.0) .with_msg(ChangeConfig::ScrollFlickSub), - scroll_dist_em: Spinner::new(0.125..=125.0, |cx: &ConfigCx, _| cx.config().borrow().scroll_dist_em) + scroll_dist_em: Spinner::new(0.125..=125.0, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_dist_em) .with_step(0.125) .with_msg(ChangeConfig::ScrollDistEm), - pan_dist_thresh: Spinner::new(0.25..=25.0, |cx: &ConfigCx, _| cx.config().borrow().pan_dist_thresh) + pan_dist_thresh: Spinner::new(0.25..=25.0, |cx: &ConfigCx, _| cx.config().borrow().event.pan_dist_thresh) .with_step(0.25) .with_msg(ChangeConfig::PanDistThresh), mouse_pan: ComboBox::new_msg( pan_options, - |cx: &ConfigCx, _| cx.config().borrow().mouse_pan, + |cx: &ConfigCx, _| cx.config().borrow().event.mouse_pan, ChangeConfig::MousePan, ), mouse_text_pan: ComboBox::new_msg( pan_options, - |cx: &ConfigCx, _| cx.config().borrow().mouse_text_pan, + |cx: &ConfigCx, _| cx.config().borrow().event.mouse_text_pan, ChangeConfig::MouseTextPan, ), mouse_nav_focus: CheckButton::new_msg( "&Mouse navigation focus", - |cx: &ConfigCx, _| cx.config().borrow().mouse_nav_focus, + |cx: &ConfigCx, _| cx.config().borrow().event.mouse_nav_focus, ChangeConfig::MouseNavFocus, ), touch_nav_focus: CheckButton::new_msg( "&Touchscreen navigation focus", - |cx: &ConfigCx, _| cx.config().borrow().touch_nav_focus, + |cx: &ConfigCx, _| cx.config().borrow().event.touch_nav_focus, ChangeConfig::TouchNavFocus, ), } diff --git a/crates/kas-widgets/src/menu/menubar.rs b/crates/kas-widgets/src/menu/menubar.rs index 11a98a22a..1d994c34a 100644 --- a/crates/kas-widgets/src/menu/menubar.rs +++ b/crates/kas-widgets/src/menu/menubar.rs @@ -194,7 +194,7 @@ impl_scope! { self.set_menu_path(cx, data, Some(&id), false); } else if id != self.delayed_open { cx.set_nav_focus(id.clone(), FocusSource::Pointer); - let delay = cx.config().menu_delay(); + let delay = cx.config().event().menu_delay(); cx.request_timer(self.id(), id.as_u64(), delay); self.delayed_open = Some(id); } diff --git a/crates/kas-widgets/src/scroll_bar.rs b/crates/kas-widgets/src/scroll_bar.rs index 0ad1dcda7..c27263c9e 100644 --- a/crates/kas-widgets/src/scroll_bar.rs +++ b/crates/kas-widgets/src/scroll_bar.rs @@ -200,7 +200,7 @@ impl_scope! { fn force_visible(&mut self, cx: &mut EventState) { self.force_visible = true; - let delay = cx.config().touch_select_delay(); + let delay = cx.config().event().touch_select_delay(); cx.request_timer(self.id(), 0, delay); } diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index 8ab499299..3d732654a 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -260,7 +260,7 @@ impl_scope! { } Event::Scroll(delta) => { let delta2 = match delta { - ScrollDelta::LineDelta(x, y) => cx.config().scroll_distance((x, y)), + ScrollDelta::LineDelta(x, y) => cx.config().event().scroll_distance((x, y)), ScrollDelta::PixelDelta(coord) => coord, }; self.pan_delta(cx, delta2) diff --git a/crates/kas-widgets/src/scroll_text.rs b/crates/kas-widgets/src/scroll_text.rs index b48b07a49..dcf13acb2 100644 --- a/crates/kas-widgets/src/scroll_text.rs +++ b/crates/kas-widgets/src/scroll_text.rs @@ -261,7 +261,7 @@ impl_scope! { } Event::Scroll(delta) => { let delta2 = match delta { - ScrollDelta::LineDelta(x, y) => cx.config().scroll_distance((x, y)), + ScrollDelta::LineDelta(x, y) => cx.config().event().scroll_distance((x, y)), ScrollDelta::PixelDelta(coord) => coord, }; self.pan_delta(cx, delta2) From 38f7da78241716575594f288e43b83666849be3f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:20:47 +0100 Subject: [PATCH 09/20] Move kas_core::theme::config -> kas_core::config::theme --- crates/kas-core/src/config/mod.rs | 2 ++ crates/kas-core/src/config/options.rs | 5 ++--- crates/kas-core/src/{theme/config.rs => config/theme.rs} | 2 +- crates/kas-core/src/draw/draw_shared.rs | 2 +- crates/kas-core/src/event/cx/config.rs | 2 +- crates/kas-core/src/theme/anim.rs | 2 +- crates/kas-core/src/theme/dimensions.rs | 5 ++--- crates/kas-core/src/theme/flat_theme.rs | 3 ++- crates/kas-core/src/theme/mod.rs | 2 -- crates/kas-core/src/theme/multi.rs | 3 ++- crates/kas-core/src/theme/simple_theme.rs | 3 ++- crates/kas-core/src/theme/theme_dst.rs | 3 ++- crates/kas-core/src/theme/traits.rs | 3 ++- crates/kas-wgpu/src/draw/draw_pipe.rs | 2 +- crates/kas-wgpu/src/draw/text_pipe.rs | 2 +- crates/kas-wgpu/src/shaded_theme.rs | 3 ++- 16 files changed, 24 insertions(+), 20 deletions(-) rename crates/kas-core/src/{theme/config.rs => config/theme.rs} (99%) diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index bfb184acd..52ea1a96d 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -18,3 +18,5 @@ pub use options::{ConfigMode, Options}; mod shortcuts; pub use shortcuts::Shortcuts; + +pub mod theme; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs index 864464bde..6543cc42c 100644 --- a/crates/kas-core/src/config/options.rs +++ b/crates/kas-core/src/config/options.rs @@ -5,7 +5,7 @@ //! Configuration options -#[cfg(feature = "serde")] use super::Format; +#[cfg(feature = "serde")] use super::{theme, Format}; use super::{Config, Error}; use crate::draw::DrawSharedImpl; use crate::theme::{Theme, ThemeConfig}; @@ -120,8 +120,7 @@ impl Options { match self.config_mode { #[cfg(feature = "serde")] ConfigMode::Read | ConfigMode::ReadWrite if self.theme_config_path.is_file() => { - let config: crate::theme::Config = - Format::guess_and_read_path(&self.theme_config_path)?; + let config: theme::Config = Format::guess_and_read_path(&self.theme_config_path)?; config.apply_startup(); // Ignore Action: UI isn't built yet let _ = theme.apply_config(&config); diff --git a/crates/kas-core/src/theme/config.rs b/crates/kas-core/src/config/theme.rs similarity index 99% rename from crates/kas-core/src/theme/config.rs rename to crates/kas-core/src/config/theme.rs index 128e9ea11..6185dbbe3 100644 --- a/crates/kas-core/src/theme/config.rs +++ b/crates/kas-core/src/config/theme.rs @@ -5,8 +5,8 @@ //! Theme configuration -use super::{ColorsSrgb, TextClass, ThemeConfig}; use crate::text::fonts::{self, AddMode, FontSelector}; +use crate::theme::{ColorsSrgb, TextClass, ThemeConfig}; use crate::Action; use std::collections::BTreeMap; use std::time::Duration; diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index ec8af102c..1f406ef34 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -8,9 +8,9 @@ use super::color::Rgba; use super::{DrawImpl, PassId}; use crate::cast::Cast; +use crate::config::theme::RasterConfig; use crate::geom::{Quad, Rect, Size}; use crate::text::{Effect, TextDisplay}; -use crate::theme::RasterConfig; use std::any::Any; use std::num::NonZeroU32; use std::rc::Rc; diff --git a/crates/kas-core/src/event/cx/config.rs b/crates/kas-core/src/event/cx/config.rs index 4d80415c9..8e00f4455 100644 --- a/crates/kas-core/src/event/cx/config.rs +++ b/crates/kas-core/src/event/cx/config.rs @@ -126,7 +126,7 @@ impl<'a> ConfigCx<'a> { /// Configure a text object /// /// This selects a font given the [`TextClass`][crate::theme::TextClass], - /// [theme configuration][crate::theme::Config] and + /// [theme configuration][crate::config::theme::Config] and /// the loaded [fonts][crate::text::fonts]. #[inline] pub fn text_configure(&self, text: &mut Text) { diff --git a/crates/kas-core/src/theme/anim.rs b/crates/kas-core/src/theme/anim.rs index e4485b2da..5edc17908 100644 --- a/crates/kas-core/src/theme/anim.rs +++ b/crates/kas-core/src/theme/anim.rs @@ -26,7 +26,7 @@ pub struct AnimState { } impl AnimState { - pub fn new(config: &super::Config) -> Self { + pub fn new(config: &crate::config::theme::Config) -> Self { let c = Config { cursor_blink_rate: config.cursor_blink_rate(), fade_dur: config.transition_fade_duration(), diff --git a/crates/kas-core/src/theme/dimensions.rs b/crates/kas-core/src/theme/dimensions.rs index 849448363..1e7aff70b 100644 --- a/crates/kas-core/src/theme/dimensions.rs +++ b/crates/kas-core/src/theme/dimensions.rs @@ -11,10 +11,9 @@ use std::f32; use std::rc::Rc; use super::anim::AnimState; -use super::{ - Config, Feature, FrameStyle, MarginStyle, MarkStyle, SizableText, TextClass, ThemeSize, -}; +use super::{Feature, FrameStyle, MarginStyle, MarkStyle, SizableText, TextClass, ThemeSize}; use crate::cast::traits::*; +use crate::config::theme::Config; use crate::dir::Directional; use crate::geom::{Rect, Size, Vec2}; use crate::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules, Stretch}; diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index 4631e7be6..fc32fb8ad 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -11,6 +11,7 @@ use std::time::Instant; use super::SimpleTheme; use crate::cast::traits::*; +use crate::config::theme::Config; use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; @@ -18,7 +19,7 @@ use crate::geom::*; use crate::text::TextDisplay; use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; -use crate::theme::{ColorsLinear, Config, InputState, Theme}; +use crate::theme::{ColorsLinear, InputState, Theme}; use crate::theme::{ThemeControl, ThemeDraw, ThemeSize}; use crate::{Action, Id}; diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs index e431d0535..45f4683fb 100644 --- a/crates/kas-core/src/theme/mod.rs +++ b/crates/kas-core/src/theme/mod.rs @@ -14,7 +14,6 @@ mod anim; mod colors; -mod config; mod draw; mod flat_theme; mod multi; @@ -30,7 +29,6 @@ mod traits; pub mod dimensions; pub use colors::{Colors, ColorsLinear, ColorsSrgb, InputState}; -pub use config::{Config, RasterConfig}; pub use draw::{Background, DrawCx}; pub use flat_theme::FlatTheme; pub use multi::{MultiTheme, MultiThemeBuilder}; diff --git a/crates/kas-core/src/theme/multi.rs b/crates/kas-core/src/theme/multi.rs index f2e5685b6..1dae456d6 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -7,7 +7,8 @@ use std::collections::HashMap; -use super::{ColorsLinear, Config, Theme, ThemeDst, Window}; +use super::{ColorsLinear, Theme, ThemeDst, Window}; +use crate::config::theme::Config; use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 97ceca9a0..2f562fb4e 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -12,6 +12,7 @@ use std::rc::Rc; use std::time::Instant; use crate::cast::traits::*; +use crate::config::theme::Config; use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; @@ -19,7 +20,7 @@ use crate::geom::*; use crate::text::{fonts, Effect, TextDisplay}; use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; -use crate::theme::{ColorsLinear, Config, InputState, Theme}; +use crate::theme::{ColorsLinear, InputState, Theme}; use crate::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; use crate::{Action, Id}; diff --git a/crates/kas-core/src/theme/theme_dst.rs b/crates/kas-core/src/theme/theme_dst.rs index ef832d318..09358edb9 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -5,7 +5,8 @@ //! Stack-DST versions of theme traits -use super::{Config, Theme, Window}; +use super::{Theme, Window}; +use crate::config::theme::Config; use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; diff --git a/crates/kas-core/src/theme/traits.rs b/crates/kas-core/src/theme/traits.rs index 5d031e930..9139f520c 100644 --- a/crates/kas-core/src/theme/traits.rs +++ b/crates/kas-core/src/theme/traits.rs @@ -5,7 +5,8 @@ //! Theme traits -use super::{ColorsLinear, ColorsSrgb, Config, RasterConfig, ThemeDraw, ThemeSize}; +use super::{ColorsLinear, ColorsSrgb, ThemeDraw, ThemeSize}; +use crate::config::theme::{Config, RasterConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; use crate::event::EventState; use crate::{autoimpl, Action}; diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 8da520135..1652cb95a 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -14,11 +14,11 @@ use crate::DrawShadedImpl; use crate::Options; use kas::app::Error; use kas::cast::traits::*; +use kas::config::theme::RasterConfig; use kas::draw::color::Rgba; use kas::draw::*; use kas::geom::{Quad, Size, Vec2}; use kas::text::{Effect, TextDisplay}; -use kas::theme::RasterConfig; /// Failure while constructing an [`Application`]: no graphics adapter found #[non_exhaustive] diff --git a/crates/kas-wgpu/src/draw/text_pipe.rs b/crates/kas-wgpu/src/draw/text_pipe.rs index b304fa4dc..1491688ce 100644 --- a/crates/kas-wgpu/src/draw/text_pipe.rs +++ b/crates/kas-wgpu/src/draw/text_pipe.rs @@ -7,11 +7,11 @@ use super::{atlases, ShaderManager}; use kas::cast::*; +use kas::config::theme::RasterConfig; use kas::draw::{color::Rgba, PassId}; use kas::geom::{Quad, Rect, Vec2}; use kas::text::fonts::FaceId; use kas::text::{Effect, Glyph, TextDisplay}; -use kas::theme::RasterConfig; use kas_text::raster::{raster, Config, SpriteDescriptor}; use rustc_hash::FxHashMap as HashMap; use std::mem::size_of; diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index d82028842..48902b776 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -11,6 +11,7 @@ use std::time::Instant; use crate::{DrawShaded, DrawShadedImpl}; use kas::cast::traits::*; +use kas::config::theme::Config; use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::event::EventState; @@ -18,7 +19,7 @@ use kas::geom::*; use kas::text::TextDisplay; use kas::theme::dimensions as dim; use kas::theme::{Background, ThemeControl, ThemeDraw, ThemeSize}; -use kas::theme::{ColorsLinear, Config, FlatTheme, InputState, SimpleTheme, Theme}; +use kas::theme::{ColorsLinear, FlatTheme, InputState, SimpleTheme, Theme}; use kas::theme::{FrameStyle, MarkStyle, TextClass}; use kas::{Action, Id}; From b39d5827f61efe91a576ee02f3f29eb7911c447e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 29 Apr 2024 16:32:10 +0100 Subject: [PATCH 10/20] Application: support direct access to config --- crates/kas-core/src/app/app.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/kas-core/src/app/app.rs b/crates/kas-core/src/app/app.rs index 3b6222d73..2206666eb 100644 --- a/crates/kas-core/src/app/app.rs +++ b/crates/kas-core/src/app/app.rs @@ -11,7 +11,7 @@ use crate::draw::{DrawShared, DrawSharedImpl}; use crate::theme::{self, Theme, ThemeConfig}; use crate::util::warn_about_error; use crate::{impl_scope, Window, WindowId}; -use std::cell::RefCell; +use std::cell::{Ref, RefCell, RefMut}; use std::rc::Rc; use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy}; @@ -165,6 +165,18 @@ where &mut self.state.shared.draw } + /// Access config + #[inline] + pub fn config(&self) -> Ref { + self.state.shared.config.borrow() + } + + /// Access config mutably + #[inline] + pub fn config_mut(&mut self) -> RefMut { + self.state.shared.config.borrow_mut() + } + /// Access the theme by ref #[inline] pub fn theme(&self) -> &T { From 2275feb117b7c760fba0aa4c4aa48b71f7994722 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:45:13 +0100 Subject: [PATCH 11/20] Move font configuration to FontConfig under base config Also rename theme::Config -> ThemeConfig --- crates/kas-core/src/app/app.rs | 5 +- crates/kas-core/src/app/mod.rs | 2 +- crates/kas-core/src/app/shared.rs | 4 +- crates/kas-core/src/app/window.rs | 37 ++-- crates/kas-core/src/config/config.rs | 26 ++- crates/kas-core/src/config/font.rs | 212 ++++++++++++++++++++++ crates/kas-core/src/config/mod.rs | 6 +- crates/kas-core/src/config/options.rs | 8 +- crates/kas-core/src/config/theme.rs | 198 +------------------- crates/kas-core/src/draw/draw_shared.rs | 2 +- crates/kas-core/src/event/cx/platform.rs | 4 +- crates/kas-core/src/theme/anim.rs | 2 +- crates/kas-core/src/theme/dimensions.rs | 19 +- crates/kas-core/src/theme/flat_theme.rs | 32 +--- crates/kas-core/src/theme/mod.rs | 2 +- crates/kas-core/src/theme/multi.rs | 30 +-- crates/kas-core/src/theme/simple_theme.rs | 36 ++-- crates/kas-core/src/theme/theme_dst.rs | 30 +-- crates/kas-core/src/theme/traits.rs | 44 +---- crates/kas-wgpu/src/draw/draw_pipe.rs | 2 +- crates/kas-wgpu/src/draw/text_pipe.rs | 2 +- crates/kas-wgpu/src/shaded_theme.rs | 31 +--- examples/calculator.rs | 9 +- examples/counter.rs | 9 +- examples/stopwatch.rs | 11 +- examples/sync-counter.rs | 8 +- examples/times-tables.rs | 2 +- 27 files changed, 376 insertions(+), 397 deletions(-) create mode 100644 crates/kas-core/src/config/font.rs diff --git a/crates/kas-core/src/app/app.rs b/crates/kas-core/src/app/app.rs index 2206666eb..25ad5d4d2 100644 --- a/crates/kas-core/src/app/app.rs +++ b/crates/kas-core/src/app/app.rs @@ -8,7 +8,7 @@ use super::{AppData, AppGraphicsBuilder, AppState, Platform, ProxyAction, Result}; use crate::config::{Config, Options}; use crate::draw::{DrawShared, DrawSharedImpl}; -use crate::theme::{self, Theme, ThemeConfig}; +use crate::theme::{self, Theme}; use crate::util::warn_about_error; use crate::{impl_scope, Window, WindowId}; use std::cell::{Ref, RefCell, RefMut}; @@ -84,11 +84,12 @@ impl_scope! { Default::default() } }); + config.borrow_mut().init(); let el = EventLoopBuilder::with_user_event().build()?; let mut draw_shared = self.graphical.build()?; - draw_shared.set_raster_config(theme.config().raster()); + draw_shared.set_raster_config(config.borrow().font.raster()); let pw = PlatformWrapper(&el); let state = AppState::new(data, pw, draw_shared, theme, options, config)?; diff --git a/crates/kas-core/src/app/mod.rs b/crates/kas-core/src/app/mod.rs index 393ab8fe4..e4722d574 100644 --- a/crates/kas-core/src/app/mod.rs +++ b/crates/kas-core/src/app/mod.rs @@ -176,7 +176,7 @@ mod test { todo!() } - fn set_raster_config(&mut self, _: &crate::theme::RasterConfig) { + fn set_raster_config(&mut self, _: &crate::config::RasterConfig) { todo!() } diff --git a/crates/kas-core/src/app/shared.rs b/crates/kas-core/src/app/shared.rs index e81d7ff1d..e2506f743 100644 --- a/crates/kas-core/src/app/shared.rs +++ b/crates/kas-core/src/app/shared.rs @@ -55,8 +55,8 @@ where config: Rc>, ) -> Result { let platform = pw.platform(); - let mut draw = kas::draw::SharedState::new(draw_shared); - theme.init(&mut draw); + let draw = kas::draw::SharedState::new(draw_shared); + theme.init(&*config.borrow()); #[cfg(feature = "clipboard")] let clipboard = match Clipboard::new() { diff --git a/crates/kas-core/src/app/window.rs b/crates/kas-core/src/app/window.rs index 33b907ec1..5c49b7020 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -79,9 +79,11 @@ impl> Window { // We cannot reliably determine the scale factor before window creation. // A factor of 1.0 lets us estimate the size requirements (logical). - let mut theme_window = state.shared.theme.new_window(1.0); - let dpem = theme_window.size().dpem(); - self.ev_state.update_config(1.0, dpem); + self.ev_state.update_config(1.0); + + let config = self.ev_state.config(); + let mut theme_window = state.shared.theme.new_window(config); + self.ev_state.full_configure( theme_window.size(), self.window_id, @@ -122,10 +124,11 @@ impl> Window { // Now that we have a scale factor, we may need to resize: let scale_factor = window.scale_factor(); if scale_factor != 1.0 { - let sf32 = scale_factor as f32; - state.shared.theme.update_window(&mut theme_window, sf32); - let dpem = theme_window.size().dpem(); - self.ev_state.update_config(sf32, dpem); + self.ev_state.update_config(scale_factor as f32); + + let config = self.ev_state.config(); + state.shared.theme.update_window(&mut theme_window, config); + let node = self.widget.as_node(&state.data); let sizer = SizeCx::new(theme_window.size()); solve_cache = SolveCache::find_constraints(node, sizer); @@ -226,13 +229,13 @@ impl> Window { } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { // Note: API allows us to set new window size here. - let scale_factor = scale_factor as f32; + self.ev_state.update_config(scale_factor as f32); + + let config = self.ev_state.config(); state .shared .theme - .update_window(&mut window.theme_window, scale_factor); - let dpem = window.theme_window.size().dpem(); - self.ev_state.update_config(scale_factor, dpem); + .update_window(&mut window.theme_window, config); // NOTE: we could try resizing here in case the window is too // small due to non-linear scaling, but it appears unnecessary. @@ -301,9 +304,7 @@ impl> Window { pub(super) fn handle_action(&mut self, state: &AppState, mut action: Action) { if action.contains(Action::EVENT_CONFIG) { if let Some(ref mut window) = self.window { - let scale_factor = window.scale_factor() as f32; - let dpem = window.theme_window.size().dpem(); - self.ev_state.update_config(scale_factor, dpem); + self.ev_state.update_config(window.scale_factor() as f32); action |= Action::UPDATE; } } @@ -314,17 +315,17 @@ impl> Window { } if action.contains(Action::THEME_SWITCH) { if let Some(ref mut window) = self.window { - let scale_factor = window.scale_factor() as f32; - window.theme_window = state.shared.theme.new_window(scale_factor); + let config = self.ev_state.config(); + window.theme_window = state.shared.theme.new_window(config); } action |= Action::RESIZE; } else if action.contains(Action::THEME_UPDATE) { if let Some(ref mut window) = self.window { - let scale_factor = window.scale_factor() as f32; + let config = self.ev_state.config(); state .shared .theme - .update_window(&mut window.theme_window, scale_factor); + .update_window(&mut window.theme_window, config); } action |= Action::RESIZE; } diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index b9ec2a1aa..2e49df0f8 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -6,6 +6,7 @@ //! Top-level configuration struct use super::event::{self, ChangeConfig}; +use super::FontConfig; use crate::cast::Cast; use crate::config::Shortcuts; use crate::Action; @@ -28,6 +29,8 @@ use std::time::Duration; pub struct Config { pub event: event::Config, + pub font: FontConfig, + #[cfg_attr(feature = "serde", serde(default = "Shortcuts::platform_defaults"))] pub shortcuts: Shortcuts, @@ -42,6 +45,7 @@ impl Default for Config { fn default() -> Self { Config { event: event::Config::default(), + font: Default::default(), shortcuts: Shortcuts::platform_defaults(), frame_dur_nanos: defaults::frame_dur_nanos(), is_dirty: false, @@ -50,6 +54,11 @@ impl Default for Config { } impl Config { + /// Call on startup + pub(crate) fn init(&mut self) { + self.font.init(); + } + /// Has the config ever been updated? #[inline] pub fn is_dirty(&self) -> bool { @@ -66,6 +75,7 @@ impl Config { #[derive(Clone, Debug)] pub struct WindowConfig { pub(super) config: Rc>, + pub(super) scale_factor: f32, pub(super) scroll_flick_sub: f32, pub(super) scroll_dist: f32, pub(super) pan_dist_thresh: f32, @@ -83,6 +93,7 @@ impl WindowConfig { pub fn new(config: Rc>) -> Self { WindowConfig { config, + scale_factor: f32::NAN, scroll_flick_sub: f32::NAN, scroll_dist: f32::NAN, pan_dist_thresh: f32::NAN, @@ -94,9 +105,11 @@ impl WindowConfig { /// Update window-specific/cached values #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn update(&mut self, scale_factor: f32, dpem: f32) { + pub fn update(&mut self, scale_factor: f32) { let base = self.config.borrow(); + self.scale_factor = scale_factor; self.scroll_flick_sub = base.event.scroll_flick_sub * scale_factor; + let dpem = base.font.size() * scale_factor; self.scroll_dist = base.event.scroll_dist_em * dpem; self.pan_dist_thresh = base.event.pan_dist_thresh * scale_factor; self.frame_dur = Duration::from_nanos(base.frame_dur_nanos.cast()); @@ -129,11 +142,22 @@ impl WindowConfig { event::WindowConfig(self) } + /// Access font config + pub fn font(&self) -> Ref { + Ref::map(self.config.borrow(), |c| &c.font) + } + /// Access shortcut config pub fn shortcuts(&self) -> Ref { Ref::map(self.config.borrow(), |c| &c.shortcuts) } + /// Scale factor + pub fn scale_factor(&self) -> f32 { + debug_assert!(self.scale_factor.is_finite()); + self.scale_factor + } + /// Minimum frame time #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] diff --git a/crates/kas-core/src/config/font.rs b/crates/kas-core/src/config/font.rs new file mode 100644 index 000000000..10e68c18d --- /dev/null +++ b/crates/kas-core/src/config/font.rs @@ -0,0 +1,212 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Font configuration + +use crate::text::fonts::{self, AddMode, FontSelector}; +use crate::theme::TextClass; +use std::collections::BTreeMap; + +/// Event handling configuration +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FontConfig { + /// Standard font size, in units of pixels-per-Em + #[cfg_attr(feature = "serde", serde(default = "defaults::size"))] + size: f32, + + /// Font aliases, used when searching for a font family matching the key. + /// + /// Example: + /// ```yaml + /// aliases: + /// sans-serif: + /// mode: Prepend + /// list: + /// - noto sans + /// ``` + /// + /// Fonts are named by *family*. Several standard families exist, e.g. + /// "serif", "sans-serif", "monospace"; these resolve to a list + /// of aliases (e.g. "Noto Sans", "DejaVu Sans", "Arial"), each of which may + /// have further aliases. + /// + /// In the above example, "noto sans" is inserted at the top of the alias + /// list for "sans-serif". + /// + /// Supported modes: `Prepend`, `Append`, `Replace`. + #[cfg_attr(feature = "serde", serde(default))] + aliases: BTreeMap, + + /// Standard fonts + #[cfg_attr(feature = "serde", serde(default))] + fonts: BTreeMap>, + + /// Text glyph rastering settings + #[cfg_attr(feature = "serde", serde(default))] + raster: RasterConfig, +} + +impl Default for FontConfig { + fn default() -> Self { + FontConfig { + size: defaults::size(), + aliases: Default::default(), + fonts: defaults::fonts(), + raster: Default::default(), + } + } +} + +/// Font raster settings +/// +/// These are not used by the theme, but passed through to the rendering +/// backend. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct RasterConfig { + //// Raster mode/engine (backend dependent) + #[cfg_attr(feature = "serde", serde(default))] + pub mode: u8, + /// Scale multiplier for fixed-precision + /// + /// This should be an integer `n >= 1`, e.g. `n = 4` provides four sub-pixel + /// steps of precision. It is also required that `n * h < (1 << 24)` where + /// `h` is the text height in pixels. + #[cfg_attr(feature = "serde", serde(default = "defaults::scale_steps"))] + pub scale_steps: u8, + /// Subpixel positioning threshold + /// + /// Text with height `h` less than this threshold will use sub-pixel + /// positioning, which should make letter spacing more accurate for small + /// fonts (though exact behaviour depends on the font; it may be worse). + /// This may make rendering worse by breaking pixel alignment. + /// + /// Note: this feature may not be available, depending on the backend and + /// the mode. + /// + /// See also sub-pixel positioning steps. + #[cfg_attr(feature = "serde", serde(default = "defaults::subpixel_threshold"))] + pub subpixel_threshold: u8, + /// Subpixel steps + /// + /// The number of sub-pixel positioning steps to use. 1 is the minimum and + /// equivalent to no sub-pixel positioning. 16 is the maximum. + /// + /// Note that since this applies to horizontal and vertical positioning, the + /// maximum number of rastered glyphs is multiplied by the square of this + /// value, though this maxmimum may not be reached in practice. Since this + /// feature is usually only used for small fonts this likely acceptable. + #[cfg_attr(feature = "serde", serde(default = "defaults::subpixel_steps"))] + pub subpixel_steps: u8, +} + +impl Default for RasterConfig { + fn default() -> Self { + RasterConfig { + mode: 0, + scale_steps: defaults::scale_steps(), + subpixel_threshold: defaults::subpixel_threshold(), + subpixel_steps: defaults::subpixel_steps(), + } + } +} + +/// Getters +impl FontConfig { + /// Standard font size + /// + /// Units: logical (unscaled) pixels per Em. + /// + /// To convert to Points, multiply by three quarters. + #[inline] + pub fn size(&self) -> f32 { + self.size + } + + /// Get an iterator over font mappings + #[inline] + pub fn iter_fonts(&self) -> impl Iterator)> { + self.fonts.iter() + } +} + +/// Setters +impl FontConfig { + /// Set standard font size + /// + /// Units: logical (unscaled) pixels per Em. + /// + /// To convert to Points, multiply by three quarters. + pub fn set_size(&mut self, pt_size: f32) { + self.size = pt_size; + } +} + +/// Other functions +impl FontConfig { + /// Apply config effects which only happen on startup + pub(super) fn init(&self) { + if !self.aliases.is_empty() { + fonts::library().update_db(|db| { + for (family, aliases) in self.aliases.iter() { + db.add_aliases( + family.to_string().into(), + aliases.list.iter().map(|s| s.to_string().into()), + aliases.mode, + ); + } + }); + } + } + + /// Get raster config + #[inline] + pub fn raster(&self) -> &RasterConfig { + &self.raster + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FontAliases { + #[cfg_attr(feature = "serde", serde(default = "defaults::add_mode"))] + mode: AddMode, + list: Vec, +} + +mod defaults { + use super::*; + + #[cfg(feature = "serde")] + pub fn add_mode() -> AddMode { + AddMode::Prepend + } + + pub fn size() -> f32 { + 16.0 + } + + pub fn fonts() -> BTreeMap> { + let mut selector = FontSelector::new(); + selector.set_families(vec!["serif".into()]); + let list = [ + (TextClass::Edit(false), selector.clone()), + (TextClass::Edit(true), selector), + ]; + list.iter().cloned().collect() + } + + pub fn scale_steps() -> u8 { + 4 + } + pub fn subpixel_threshold() -> u8 { + 0 + } + pub fn subpixel_steps() -> u8 { + 5 + } +} diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index 52ea1a96d..5fd4dd2ab 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -10,6 +10,9 @@ pub use config::{Config, WindowConfig}; pub mod event; +mod font; +pub use font::{FontConfig, RasterConfig}; + mod format; pub use format::{Error, Format}; @@ -19,4 +22,5 @@ pub use options::{ConfigMode, Options}; mod shortcuts; pub use shortcuts::Shortcuts; -pub mod theme; +mod theme; +pub use theme::ThemeConfig; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs index 6543cc42c..8a87cf232 100644 --- a/crates/kas-core/src/config/options.rs +++ b/crates/kas-core/src/config/options.rs @@ -8,7 +8,7 @@ #[cfg(feature = "serde")] use super::{theme, Format}; use super::{Config, Error}; use crate::draw::DrawSharedImpl; -use crate::theme::{Theme, ThemeConfig}; +use crate::theme::Theme; #[cfg(feature = "serde")] use crate::util::warn_about_error; use std::env::var; use std::path::PathBuf; @@ -121,21 +121,21 @@ impl Options { #[cfg(feature = "serde")] ConfigMode::Read | ConfigMode::ReadWrite if self.theme_config_path.is_file() => { let config: theme::Config = Format::guess_and_read_path(&self.theme_config_path)?; - config.apply_startup(); // Ignore Action: UI isn't built yet let _ = theme.apply_config(&config); } #[cfg(feature = "serde")] ConfigMode::WriteDefault if !self.theme_config_path.as_os_str().is_empty() => { let config = theme.config(); - config.apply_startup(); if let Err(error) = Format::guess_and_write_path(&self.theme_config_path, config.as_ref()) { warn_about_error("failed to write default config: ", &error); } } - _ => theme.config().apply_startup(), + _ => { + let _ = theme; + } } Ok(()) diff --git a/crates/kas-core/src/config/theme.rs b/crates/kas-core/src/config/theme.rs index 6185dbbe3..b7bd9206d 100644 --- a/crates/kas-core/src/config/theme.rs +++ b/crates/kas-core/src/config/theme.rs @@ -5,8 +5,7 @@ //! Theme configuration -use crate::text::fonts::{self, AddMode, FontSelector}; -use crate::theme::{ColorsSrgb, TextClass, ThemeConfig}; +use crate::theme::ColorsSrgb; use crate::Action; use std::collections::BTreeMap; use std::time::Duration; @@ -14,14 +13,10 @@ use std::time::Duration; /// Event handling configuration #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Config { +pub struct ThemeConfig { #[cfg_attr(feature = "serde", serde(skip))] dirty: bool, - /// Standard font size, in units of pixels-per-Em - #[cfg_attr(feature = "serde", serde(default = "defaults::font_size"))] - font_size: f32, - /// The colour scheme to use #[cfg_attr(feature = "serde", serde(default))] active_scheme: String, @@ -32,33 +27,6 @@ pub struct Config { #[cfg_attr(feature = "serde", serde(default = "defaults::color_schemes"))] color_schemes: BTreeMap, - /// Font aliases, used when searching for a font family matching the key. - /// - /// Example: - /// ```yaml - /// font_aliases: - /// sans-serif: - /// mode: Prepend - /// list: - /// - noto sans - /// ``` - /// - /// Fonts are named by *family*. Several standard families exist, e.g. - /// "serif", "sans-serif", "monospace"; these resolve to a list - /// of aliases (e.g. "Noto Sans", "DejaVu Sans", "Arial"), each of which may - /// have further aliases. - /// - /// In the above example, "noto sans" is inserted at the top of the alias - /// list for "sans-serif". - /// - /// Supported modes: `Prepend`, `Append`, `Replace`. - #[cfg_attr(feature = "serde", serde(default))] - font_aliases: BTreeMap, - - /// Standard fonts - #[cfg_attr(feature = "serde", serde(default))] - fonts: BTreeMap>, - /// Text cursor blink rate: delay between switching states #[cfg_attr(feature = "serde", serde(default = "defaults::cursor_blink_rate_ms"))] cursor_blink_rate_ms: u32, @@ -66,94 +34,22 @@ pub struct Config { /// Transition duration used in animations #[cfg_attr(feature = "serde", serde(default = "defaults::transition_fade_ms"))] transition_fade_ms: u32, - - /// Text glyph rastering settings - #[cfg_attr(feature = "serde", serde(default))] - raster: RasterConfig, } -impl Default for Config { +impl Default for ThemeConfig { fn default() -> Self { - Config { + ThemeConfig { dirty: false, - font_size: defaults::font_size(), active_scheme: Default::default(), color_schemes: defaults::color_schemes(), - font_aliases: Default::default(), - fonts: defaults::fonts(), cursor_blink_rate_ms: defaults::cursor_blink_rate_ms(), transition_fade_ms: defaults::transition_fade_ms(), - raster: Default::default(), - } - } -} - -/// Font raster settings -/// -/// These are not used by the theme, but passed through to the rendering -/// backend. -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct RasterConfig { - //// Raster mode/engine (backend dependent) - #[cfg_attr(feature = "serde", serde(default))] - pub mode: u8, - /// Scale multiplier for fixed-precision - /// - /// This should be an integer `n >= 1`, e.g. `n = 4` provides four sub-pixel - /// steps of precision. It is also required that `n * h < (1 << 24)` where - /// `h` is the text height in pixels. - #[cfg_attr(feature = "serde", serde(default = "defaults::scale_steps"))] - pub scale_steps: u8, - /// Subpixel positioning threshold - /// - /// Text with height `h` less than this threshold will use sub-pixel - /// positioning, which should make letter spacing more accurate for small - /// fonts (though exact behaviour depends on the font; it may be worse). - /// This may make rendering worse by breaking pixel alignment. - /// - /// Note: this feature may not be available, depending on the backend and - /// the mode. - /// - /// See also sub-pixel positioning steps. - #[cfg_attr(feature = "serde", serde(default = "defaults::subpixel_threshold"))] - pub subpixel_threshold: u8, - /// Subpixel steps - /// - /// The number of sub-pixel positioning steps to use. 1 is the minimum and - /// equivalent to no sub-pixel positioning. 16 is the maximum. - /// - /// Note that since this applies to horizontal and vertical positioning, the - /// maximum number of rastered glyphs is multiplied by the square of this - /// value, though this maxmimum may not be reached in practice. Since this - /// feature is usually only used for small fonts this likely acceptable. - #[cfg_attr(feature = "serde", serde(default = "defaults::subpixel_steps"))] - pub subpixel_steps: u8, -} - -impl Default for RasterConfig { - fn default() -> Self { - RasterConfig { - mode: 0, - scale_steps: defaults::scale_steps(), - subpixel_threshold: defaults::subpixel_threshold(), - subpixel_steps: defaults::subpixel_steps(), } } } /// Getters -impl Config { - /// Standard font size - /// - /// Units: logical (unscaled) pixels per Em. - /// - /// To convert to Points, multiply by three quarters. - #[inline] - pub fn font_size(&self) -> f32 { - self.font_size - } - +impl ThemeConfig { /// Active colour scheme (name) /// /// An empty string will resolve the default colour scheme. @@ -182,12 +78,6 @@ impl Config { self.color_schemes.get(&self.active_scheme).cloned() } - /// Get an iterator over font mappings - #[inline] - pub fn iter_fonts(&self) -> impl Iterator)> { - self.fonts.iter() - } - /// Get the cursor blink rate (delay) #[inline] pub fn cursor_blink_rate(&self) -> Duration { @@ -202,13 +92,7 @@ impl Config { } /// Setters -impl Config { - /// Set font size - pub fn set_font_size(&mut self, pt_size: f32) { - self.dirty = true; - self.font_size = pt_size; - } - +impl ThemeConfig { /// Set colour scheme pub fn set_active_scheme(&mut self, scheme: impl ToString) { self.dirty = true; @@ -217,59 +101,21 @@ impl Config { } /// Other functions -impl Config { +impl ThemeConfig { /// Currently this is just "set". Later, maybe some type of merge. #[allow(clippy::float_cmp)] - pub fn apply_config(&mut self, other: &Config) -> Action { - let action = if self.font_size != other.font_size { - Action::THEME_UPDATE - } else if self != other { - Action::REDRAW - } else { - Action::empty() - }; + pub fn apply_config(&mut self, other: &ThemeConfig) -> Action { + let action = if self != other { Action::REDRAW } else { Action::empty() }; *self = other.clone(); action } -} -impl ThemeConfig for Config { #[cfg(feature = "serde")] #[inline] - fn is_dirty(&self) -> bool { + pub(crate) fn is_dirty(&self) -> bool { self.dirty } - - /// Apply config effects which only happen on startup - fn apply_startup(&self) { - if !self.font_aliases.is_empty() { - fonts::library().update_db(|db| { - for (family, aliases) in self.font_aliases.iter() { - db.add_aliases( - family.to_string().into(), - aliases.list.iter().map(|s| s.to_string().into()), - aliases.mode, - ); - } - }); - } - } - - /// Get raster config - #[inline] - fn raster(&self) -> &RasterConfig { - &self.raster - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FontAliases { - #[cfg_attr(feature = "serde", serde(default = "defaults::add_mode"))] - mode: AddMode, - list: Vec, } mod defaults { @@ -280,10 +126,6 @@ mod defaults { AddMode::Prepend } - pub fn font_size() -> f32 { - 16.0 - } - pub fn color_schemes() -> BTreeMap { let mut schemes = BTreeMap::new(); schemes.insert("light".to_string(), ColorsSrgb::light()); @@ -292,16 +134,6 @@ mod defaults { schemes } - pub fn fonts() -> BTreeMap> { - let mut selector = FontSelector::new(); - selector.set_families(vec!["serif".into()]); - let list = [ - (TextClass::Edit(false), selector.clone()), - (TextClass::Edit(true), selector), - ]; - list.iter().cloned().collect() - } - pub fn cursor_blink_rate_ms() -> u32 { 600 } @@ -309,14 +141,4 @@ mod defaults { pub fn transition_fade_ms() -> u32 { 150 } - - pub fn scale_steps() -> u8 { - 4 - } - pub fn subpixel_threshold() -> u8 { - 0 - } - pub fn subpixel_steps() -> u8 { - 5 - } } diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index 1f406ef34..fda2bc720 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -8,7 +8,7 @@ use super::color::Rgba; use super::{DrawImpl, PassId}; use crate::cast::Cast; -use crate::config::theme::RasterConfig; +use crate::config::RasterConfig; use crate::geom::{Quad, Rect, Size}; use crate::text::{Effect, TextDisplay}; use std::any::Any; diff --git a/crates/kas-core/src/event/cx/platform.rs b/crates/kas-core/src/event/cx/platform.rs index 83e2cf28f..b9409dea5 100644 --- a/crates/kas-core/src/event/cx/platform.rs +++ b/crates/kas-core/src/event/cx/platform.rs @@ -62,8 +62,8 @@ impl EventState { } /// Update scale factor - pub(crate) fn update_config(&mut self, scale_factor: f32, dpem: f32) { - self.config.update(scale_factor, dpem); + pub(crate) fn update_config(&mut self, scale_factor: f32) { + self.config.update(scale_factor); } /// Configure a widget tree diff --git a/crates/kas-core/src/theme/anim.rs b/crates/kas-core/src/theme/anim.rs index 5edc17908..82df65e96 100644 --- a/crates/kas-core/src/theme/anim.rs +++ b/crates/kas-core/src/theme/anim.rs @@ -26,7 +26,7 @@ pub struct AnimState { } impl AnimState { - pub fn new(config: &crate::config::theme::Config) -> Self { + pub fn new(config: &crate::config::ThemeConfig) -> Self { let c = Config { cursor_blink_rate: config.cursor_blink_rate(), fade_dur: config.transition_fade_duration(), diff --git a/crates/kas-core/src/theme/dimensions.rs b/crates/kas-core/src/theme/dimensions.rs index 1e7aff70b..92c69d3fa 100644 --- a/crates/kas-core/src/theme/dimensions.rs +++ b/crates/kas-core/src/theme/dimensions.rs @@ -13,7 +13,7 @@ use std::rc::Rc; use super::anim::AnimState; use super::{Feature, FrameStyle, MarginStyle, MarkStyle, SizableText, TextClass, ThemeSize}; use crate::cast::traits::*; -use crate::config::theme::Config; +use crate::config::{ThemeConfig, WindowConfig}; use crate::dir::Directional; use crate::geom::{Rect, Size, Vec2}; use crate::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules, Stretch}; @@ -111,8 +111,9 @@ pub struct Dimensions { } impl Dimensions { - pub fn new(params: &Parameters, font_size: f32, scale: f32) -> Self { - let dpem = scale * font_size; + pub fn new(params: &Parameters, config: &WindowConfig) -> Self { + let scale = config.scale_factor(); + let dpem = scale * config.font().size(); let text_m0 = (params.m_text.0 * scale).cast_nearest(); let text_m1 = (params.m_text.1 * scale).cast_nearest(); @@ -158,19 +159,19 @@ pub struct Window { impl Window { pub fn new( dims: &Parameters, - config: &Config, - scale: f32, + config: &WindowConfig, + theme_config: &ThemeConfig, fonts: Rc>, ) -> Self { Window { - dims: Dimensions::new(dims, config.font_size(), scale), + dims: Dimensions::new(dims, config), fonts, - anim: AnimState::new(config), + anim: AnimState::new(theme_config), } } - pub fn update(&mut self, dims: &Parameters, config: &Config, scale: f32) { - self.dims = Dimensions::new(dims, config.font_size(), scale); + pub fn update(&mut self, dims: &Parameters, config: &WindowConfig) { + self.dims = Dimensions::new(dims, config); } } diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index fc32fb8ad..6b055116f 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -11,7 +11,7 @@ use std::time::Instant; use super::SimpleTheme; use crate::cast::traits::*; -use crate::config::theme::Config; +use crate::config::{Config, ThemeConfig, WindowConfig}; use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; @@ -62,16 +62,6 @@ impl FlatTheme { FlatTheme { base } } - /// Set font size - /// - /// Units: Points per Em (standard unit of font size) - #[inline] - #[must_use] - pub fn with_font_size(mut self, pt_size: f32) -> Self { - self.base = self.base.with_font_size(pt_size); - self - } - /// Set the colour scheme /// /// If no scheme by this name is found the scheme is left unchanged. @@ -109,25 +99,25 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { >::config(&self.base) } - fn apply_config(&mut self, config: &Config) -> Action { + fn apply_config(&mut self, config: &ThemeConfig) -> Action { >::apply_config(&mut self.base, config) } - fn init(&mut self, shared: &mut SharedState) { - >::init(&mut self.base, shared) + fn init(&mut self, config: &Config) { + >::init(&mut self.base, config) } - fn new_window(&self, dpi_factor: f32) -> Self::Window { + fn new_window(&self, config: &WindowConfig) -> Self::Window { let fonts = self.base.fonts.as_ref().unwrap().clone(); - dim::Window::new(&dimensions(), &self.base.config, dpi_factor, fonts) + dim::Window::new(&dimensions(), config, &self.base.config, fonts) } - fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) { - w.update(&dimensions(), &self.base.config, dpi_factor); + fn update_window(&self, w: &mut Self::Window, config: &WindowConfig) { + w.update(&dimensions(), config); } fn draw<'a>( @@ -161,10 +151,6 @@ where } impl ThemeControl for FlatTheme { - fn set_font_size(&mut self, pt_size: f32) -> Action { - self.base.set_font_size(pt_size) - } - fn active_scheme(&self) -> &str { self.base.active_scheme() } diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs index 45f4683fb..ca50099d7 100644 --- a/crates/kas-core/src/theme/mod.rs +++ b/crates/kas-core/src/theme/mod.rs @@ -37,6 +37,6 @@ pub use size::SizeCx; pub use style::*; pub use text::{SizableText, Text}; pub use theme_dst::ThemeDst; -pub use traits::{Theme, ThemeConfig, ThemeControl, Window}; +pub use traits::{Theme, ThemeControl, Window}; #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] pub use {draw::ThemeDraw, size::ThemeSize}; diff --git a/crates/kas-core/src/theme/multi.rs b/crates/kas-core/src/theme/multi.rs index 1dae456d6..25ff0deda 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; use super::{ColorsLinear, Theme, ThemeDst, Window}; -use crate::config::theme::Config; -use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; +use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; use crate::Action; @@ -82,11 +82,11 @@ impl Theme for MultiTheme { type Window = Box; type Draw<'a> = Box; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { self.themes[self.active].config() } - fn apply_config(&mut self, config: &Config) -> Action { + fn apply_config(&mut self, config: &ThemeConfig) -> Action { let mut action = Action::empty(); for theme in &mut self.themes { action |= theme.apply_config(config); @@ -94,18 +94,18 @@ impl Theme for MultiTheme { action } - fn init(&mut self, shared: &mut SharedState) { + fn init(&mut self, config: &Config) { for theme in &mut self.themes { - theme.init(shared); + theme.init(config); } } - fn new_window(&self, dpi_factor: f32) -> Self::Window { - self.themes[self.active].new_window(dpi_factor) + fn new_window(&self, config: &WindowConfig) -> Self::Window { + self.themes[self.active].new_window(config) } - fn update_window(&self, window: &mut Self::Window, dpi_factor: f32) { - self.themes[self.active].update_window(window, dpi_factor); + fn update_window(&self, window: &mut Self::Window, config: &WindowConfig) { + self.themes[self.active].update_window(window, config); } fn draw<'a>( @@ -132,16 +132,6 @@ impl Theme for MultiTheme { } impl ThemeControl for MultiTheme { - fn set_font_size(&mut self, size: f32) -> Action { - // Slightly inefficient, but sufficient: update both - // (Otherwise we would have to call set_scheme in set_theme too.) - let mut action = Action::empty(); - for theme in &mut self.themes { - action |= theme.set_font_size(size); - } - action - } - fn active_scheme(&self) -> &str { self.themes[self.active].active_scheme() } diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 2f562fb4e..66ea7dfcd 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -12,7 +12,7 @@ use std::rc::Rc; use std::time::Instant; use crate::cast::traits::*; -use crate::config::theme::Config; +use crate::config::{Config, ThemeConfig, WindowConfig}; use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; @@ -30,7 +30,7 @@ use crate::{Action, Id}; /// other themes. #[derive(Clone, Debug)] pub struct SimpleTheme { - pub config: Config, + pub config: ThemeConfig, pub cols: ColorsLinear, dims: dim::Parameters, pub fonts: Option>>, @@ -55,16 +55,6 @@ impl SimpleTheme { } } - /// Set font size - /// - /// Units: Points per Em (standard unit of font size) - #[inline] - #[must_use] - pub fn with_font_size(mut self, pt_size: f32) -> Self { - self.config.set_font_size(pt_size); - self - } - /// Set the colour scheme /// /// If no scheme by this name is found the scheme is left unchanged. @@ -90,11 +80,11 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { std::borrow::Cow::Borrowed(&self.config) } - fn apply_config(&mut self, config: &Config) -> Action { + fn apply_config(&mut self, config: &ThemeConfig) -> Action { let mut action = self.config.apply_config(config); if let Some(cols) = self.config.get_active_scheme() { self.cols = cols.into(); @@ -103,26 +93,27 @@ where action } - fn init(&mut self, _shared: &mut SharedState) { + fn init(&mut self, config: &Config) { let fonts = fonts::library(); if let Err(e) = fonts.select_default() { panic!("Error loading font: {e}"); } self.fonts = Some(Rc::new( - self.config + config + .font .iter_fonts() .filter_map(|(c, s)| fonts.select_font(s).ok().map(|id| (*c, id))) .collect(), )); } - fn new_window(&self, dpi_factor: f32) -> Self::Window { + fn new_window(&self, config: &WindowConfig) -> Self::Window { let fonts = self.fonts.as_ref().unwrap().clone(); - dim::Window::new(&self.dims, &self.config, dpi_factor, fonts) + dim::Window::new(&self.dims, config, &self.config, fonts) } - fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) { - w.update(&self.dims, &self.config, dpi_factor); + fn update_window(&self, w: &mut Self::Window, config: &WindowConfig) { + w.update(&self.dims, config); } fn draw<'a>( @@ -156,11 +147,6 @@ where } impl ThemeControl for SimpleTheme { - fn set_font_size(&mut self, pt_size: f32) -> Action { - self.config.set_font_size(pt_size); - Action::THEME_UPDATE - } - fn active_scheme(&self) -> &str { self.config.active_scheme() } diff --git a/crates/kas-core/src/theme/theme_dst.rs b/crates/kas-core/src/theme/theme_dst.rs index 09358edb9..e3edca10e 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -6,8 +6,8 @@ //! Stack-DST versions of theme traits use super::{Theme, Window}; -use crate::config::theme::Config; -use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; +use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; use crate::Action; @@ -19,25 +19,25 @@ use crate::Action; /// trait is required. pub trait ThemeDst: ThemeControl { /// Get current configuration - fn config(&self) -> std::borrow::Cow; + fn config(&self) -> std::borrow::Cow; /// Apply/set the passed config - fn apply_config(&mut self, config: &Config) -> Action; + fn apply_config(&mut self, config: &ThemeConfig) -> Action; /// Theme initialisation /// /// See also [`Theme::init`]. - fn init(&mut self, shared: &mut SharedState); + fn init(&mut self, config: &Config); /// Construct per-window storage /// /// See also [`Theme::new_window`]. - fn new_window(&self, dpi_factor: f32) -> Box; + fn new_window(&self, config: &WindowConfig) -> Box; /// Update a window created by [`Theme::new_window`] /// /// See also [`Theme::update_window`]. - fn update_window(&self, window: &mut dyn Window, dpi_factor: f32); + fn update_window(&self, window: &mut dyn Window, config: &WindowConfig); fn draw<'a>( &'a self, @@ -53,26 +53,26 @@ pub trait ThemeDst: ThemeControl { } impl> ThemeDst for T { - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { self.config() } - fn apply_config(&mut self, config: &Config) -> Action { + fn apply_config(&mut self, config: &ThemeConfig) -> Action { self.apply_config(config) } - fn init(&mut self, shared: &mut SharedState) { - self.init(shared); + fn init(&mut self, config: &Config) { + self.init(config); } - fn new_window(&self, dpi_factor: f32) -> Box { - let window = >::new_window(self, dpi_factor); + fn new_window(&self, config: &WindowConfig) -> Box { + let window = >::new_window(self, config); Box::new(window) } - fn update_window(&self, window: &mut dyn Window, dpi_factor: f32) { + fn update_window(&self, window: &mut dyn Window, config: &WindowConfig) { let window = window.as_any_mut().downcast_mut().unwrap(); - self.update_window(window, dpi_factor); + self.update_window(window, config); } fn draw<'b>( diff --git a/crates/kas-core/src/theme/traits.rs b/crates/kas-core/src/theme/traits.rs index 9139f520c..f28dd320e 100644 --- a/crates/kas-core/src/theme/traits.rs +++ b/crates/kas-core/src/theme/traits.rs @@ -6,8 +6,8 @@ //! Theme traits use super::{ColorsLinear, ColorsSrgb, ThemeDraw, ThemeSize}; -use crate::config::theme::{Config, RasterConfig}; -use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; +use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; use crate::{autoimpl, Action}; use std::any::Any; @@ -21,11 +21,6 @@ use std::any::Any; /// the UI is started, this return value can be safely ignored. #[crate::autoimpl(for &mut T, Box)] pub trait ThemeControl { - /// Set font size - /// - /// Units: Points per Em (standard unit of font size) - fn set_font_size(&mut self, pt_size: f32) -> Action; - /// Get the name of the active color scheme fn active_scheme(&self) -> &str; @@ -67,31 +62,6 @@ pub trait ThemeControl { } } -/// Requirements on theme config (without `config` feature) -#[cfg(not(feature = "serde"))] -pub trait ThemeConfig: Clone + std::fmt::Debug + 'static { - /// Apply startup effects - fn apply_startup(&self); - - /// Get raster config - fn raster(&self) -> &RasterConfig; -} - -/// Requirements on theme config (with `config` feature) -#[cfg(feature = "serde")] -pub trait ThemeConfig: - Clone + std::fmt::Debug + 'static + for<'a> serde::Deserialize<'a> + serde::Serialize -{ - /// Has the config ever been updated? - fn is_dirty(&self) -> bool; - - /// Apply startup effects - fn apply_startup(&self); - - /// Get raster config - fn raster(&self) -> &RasterConfig; -} - /// A *theme* provides widget sizing and drawing implementations. /// /// The theme is generic over some `DrawIface`. @@ -110,10 +80,10 @@ pub trait Theme: ThemeControl { Self: 'a; /// Get current configuration - fn config(&self) -> std::borrow::Cow; + fn config(&self) -> std::borrow::Cow; /// Apply/set the passed config - fn apply_config(&mut self, config: &Config) -> Action; + fn apply_config(&mut self, config: &ThemeConfig) -> Action; /// Theme initialisation /// @@ -122,7 +92,7 @@ pub trait Theme: ThemeControl { /// /// At a minimum, a theme must load a font to [`crate::text::fonts`]. /// The first font loaded (by any theme) becomes the default font. - fn init(&mut self, shared: &mut SharedState); + fn init(&mut self, config: &Config); /// Construct per-window storage /// @@ -135,12 +105,12 @@ pub trait Theme: ThemeControl { /// ``` /// /// A reference to the draw backend is provided allowing configuration. - fn new_window(&self, dpi_factor: f32) -> Self::Window; + fn new_window(&self, config: &WindowConfig) -> Self::Window; /// Update a window created by [`Theme::new_window`] /// /// This is called when the DPI factor changes or theme dimensions change. - fn update_window(&self, window: &mut Self::Window, dpi_factor: f32); + fn update_window(&self, window: &mut Self::Window, config: &WindowConfig); /// Prepare to draw and construct a [`ThemeDraw`] object /// diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 1652cb95a..2e0170d2b 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -14,7 +14,7 @@ use crate::DrawShadedImpl; use crate::Options; use kas::app::Error; use kas::cast::traits::*; -use kas::config::theme::RasterConfig; +use kas::config::RasterConfig; use kas::draw::color::Rgba; use kas::draw::*; use kas::geom::{Quad, Size, Vec2}; diff --git a/crates/kas-wgpu/src/draw/text_pipe.rs b/crates/kas-wgpu/src/draw/text_pipe.rs index 1491688ce..ad8ada6e7 100644 --- a/crates/kas-wgpu/src/draw/text_pipe.rs +++ b/crates/kas-wgpu/src/draw/text_pipe.rs @@ -7,7 +7,7 @@ use super::{atlases, ShaderManager}; use kas::cast::*; -use kas::config::theme::RasterConfig; +use kas::config::RasterConfig; use kas::draw::{color::Rgba, PassId}; use kas::geom::{Quad, Rect, Vec2}; use kas::text::fonts::FaceId; diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index 48902b776..9f5f64b09 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -11,7 +11,7 @@ use std::time::Instant; use crate::{DrawShaded, DrawShadedImpl}; use kas::cast::traits::*; -use kas::config::theme::Config; +use kas::config::{Config, ThemeConfig, WindowConfig}; use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::event::EventState; @@ -42,15 +42,6 @@ impl ShadedTheme { ShadedTheme { base } } - /// Set font size - /// - /// Units: Points per Em (standard unit of font size) - #[must_use] - pub fn with_font_size(mut self, pt_size: f32) -> Self { - self.base = self.base.with_font_size(pt_size); - self - } - /// Set the colour scheme /// /// If no scheme by this name is found the scheme is left unchanged. @@ -96,25 +87,25 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { + fn config(&self) -> std::borrow::Cow { >::config(&self.base) } - fn apply_config(&mut self, config: &Config) -> Action { + fn apply_config(&mut self, config: &ThemeConfig) -> Action { >::apply_config(&mut self.base, config) } - fn init(&mut self, shared: &mut SharedState) { - >::init(&mut self.base, shared) + fn init(&mut self, config: &Config) { + >::init(&mut self.base, config) } - fn new_window(&self, dpi_factor: f32) -> Self::Window { + fn new_window(&self, config: &WindowConfig) -> Self::Window { let fonts = self.base.fonts.as_ref().unwrap().clone(); - dim::Window::new(&dimensions(), &self.base.config, dpi_factor, fonts) + dim::Window::new(&dimensions(), config, &self.base.config, fonts) } - fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) { - w.update(&dimensions(), &self.base.config, dpi_factor); + fn update_window(&self, w: &mut Self::Window, config: &WindowConfig) { + w.update(&dimensions(), config); } fn draw<'a>( @@ -148,10 +139,6 @@ where } impl ThemeControl for ShadedTheme { - fn set_font_size(&mut self, pt_size: f32) -> Action { - self.base.set_font_size(pt_size) - } - fn active_scheme(&self) -> &str { self.base.active_scheme() } diff --git a/examples/calculator.rs b/examples/calculator.rs index fd20790a9..ea6990e27 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -72,11 +72,10 @@ fn calc_ui() -> Window<()> { fn main() -> kas::app::Result<()> { env_logger::init(); - let theme = kas_wgpu::ShadedTheme::new().with_font_size(16.0); - kas::app::Default::with_theme(theme) - .build(())? - .with(calc_ui()) - .run() + let theme = kas_wgpu::ShadedTheme::new(); + let mut app = kas::app::Default::with_theme(theme).build(())?; + app.config_mut().font.set_size(24.0); + app.with(calc_ui()).run() } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/examples/counter.rs b/examples/counter.rs index 9633fcc6b..da8acfe16 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -27,9 +27,8 @@ fn counter() -> impl Widget { fn main() -> kas::app::Result<()> { env_logger::init(); - let theme = kas::theme::SimpleTheme::new().with_font_size(24.0); - kas::app::Default::with_theme(theme) - .build(())? - .with(Window::new(counter(), "Counter")) - .run() + let theme = kas::theme::SimpleTheme::new(); + let mut app = kas::app::Default::with_theme(theme).build(())?; + app.config_mut().font.set_size(24.0); + app.with(Window::new(counter(), "Counter")).run() } diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 7617eafa5..d82a84b01 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -59,11 +59,8 @@ fn main() -> kas::app::Result<()> { .with_transparent(true) .with_restrictions(true, true); - let theme = kas_wgpu::ShadedTheme::new() - .with_colours("dark") - .with_font_size(18.0); - kas::app::Default::with_theme(theme) - .build(())? - .with(window) - .run() + let theme = kas_wgpu::ShadedTheme::new().with_colours("dark"); + let mut app = kas::app::Default::with_theme(theme).build(())?; + app.config_mut().font.set_size(24.0); + app.with(window).run() } diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs index 5aac3e222..26737021e 100644 --- a/examples/sync-counter.rs +++ b/examples/sync-counter.rs @@ -56,11 +56,11 @@ fn main() -> kas::app::Result<()> { env_logger::init(); let count = Count(0); - let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0); + let theme = kas_wgpu::ShadedTheme::new(); - kas::app::Default::with_theme(theme) - .build(count)? - .with(counter("Counter 1")) + let mut app = kas::app::Default::with_theme(theme).build(count)?; + app.config_mut().font.set_size(24.0); + app.with(counter("Counter 1")) .with(counter("Counter 2")) .run() } diff --git a/examples/times-tables.rs b/examples/times-tables.rs index dd4ffc726..9436d265e 100644 --- a/examples/times-tables.rs +++ b/examples/times-tables.rs @@ -74,7 +74,7 @@ fn main() -> kas::app::Result<()> { }); let window = Window::new(ui, "Times-Tables"); - let theme = kas::theme::SimpleTheme::new().with_font_size(16.0); + let theme = kas::theme::SimpleTheme::new(); kas::app::Default::with_theme(theme) .build(())? .with(window) From b43c8817100a6215de678602e6119d782398ee40 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 30 Apr 2024 12:38:35 +0100 Subject: [PATCH 12/20] Add filter to WidgetHierarchy F8 now shows a filtered widget tree from the window root --- crates/kas-core/src/event/cx/mod.rs | 15 +++++--------- crates/kas-core/src/layout/sizer.rs | 2 +- crates/kas-core/src/util.rs | 31 +++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index a9dd7683f..bccaec4fc 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -23,7 +23,6 @@ use crate::config::WindowConfig; use crate::geom::Coord; use crate::messages::{Erased, MessageStack}; use crate::util::WidgetHierarchy; -use crate::LayoutExt; use crate::{Action, Id, NavAdvance, Node, WindowId}; mod config; @@ -439,15 +438,11 @@ impl<'a> EventCx<'a> { } if matches!(cmd, Command::Debug) { - if let Some(ref id) = self.hover { - if let Some(w) = widget.as_layout().find_widget(id) { - let hier = WidgetHierarchy::new(w); - log::debug!("Widget heirarchy (from mouse): {hier}"); - } - } else { - let hier = WidgetHierarchy::new(widget.as_layout()); - log::debug!("Widget heirarchy (whole window): {hier}"); - } + let hier = WidgetHierarchy::new(widget.as_layout(), self.hover.clone()); + log::debug!( + "Widget heirarchy (filter={:?}): {hier}", + self.hover.as_ref() + ); return; } } diff --git a/crates/kas-core/src/layout/sizer.rs b/crates/kas-core/src/layout/sizer.rs index f94f64404..21736e364 100644 --- a/crates/kas-core/src/layout/sizer.rs +++ b/crates/kas-core/src/layout/sizer.rs @@ -226,7 +226,7 @@ impl SolveCache { /// This is sometimes called after [`Self::apply_rect`]. pub fn print_widget_heirarchy(&mut self, widget: &dyn Layout) { let rect = widget.rect(); - let hier = WidgetHierarchy::new(widget); + let hier = WidgetHierarchy::new(widget, None); log::trace!( target: "kas_core::layout::hierarchy", "apply_rect: rect={rect:?}:{hier}", diff --git a/crates/kas-core/src/util.rs b/crates/kas-core/src/util.rs index b308992ac..2cccefd15 100644 --- a/crates/kas-core/src/util.rs +++ b/crates/kas-core/src/util.rs @@ -24,11 +24,16 @@ impl<'a> fmt::Display for IdentifyWidget<'a> { /// Note: output starts with a new line. pub struct WidgetHierarchy<'a> { widget: &'a dyn Layout, + filter: Option, indent: usize, } impl<'a> WidgetHierarchy<'a> { - pub fn new(widget: &'a dyn Layout) -> Self { - WidgetHierarchy { widget, indent: 0 } + pub fn new(widget: &'a dyn Layout, filter: Option) -> Self { + WidgetHierarchy { + widget, + filter, + indent: 0, + } } } impl<'a> fmt::Display for WidgetHierarchy<'a> { @@ -45,8 +50,26 @@ impl<'a> fmt::Display for WidgetHierarchy<'a> { write!(f, "\n{trail}{identify: Date: Tue, 30 Apr 2024 12:53:36 +0100 Subject: [PATCH 13/20] Fix: MapAny::_update must recurse It is valid to pass data via other means. This fixes updates to EventConfig's Spinner fields on edit. --- crates/kas-core/src/hidden.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/kas-core/src/hidden.rs b/crates/kas-core/src/hidden.rs index b67015a55..dffface2f 100644 --- a/crates/kas-core/src/hidden.rs +++ b/crates/kas-core/src/hidden.rs @@ -181,7 +181,9 @@ impl_scope! { self.inner._configure(cx, &(), id); } - fn _update(&mut self, _: &mut ConfigCx, _: &A) {} + fn _update(&mut self, cx: &mut ConfigCx, _: &A) { + self.inner._update(cx, &()); + } fn _send(&mut self, cx: &mut EventCx, _: &A, id: Id, event: Event) -> IsUsed { self.inner._send(cx, &(), id, event) From abab4b8d1acd7ac254c54b1e1fc595341a4fd18a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:45:29 +0100 Subject: [PATCH 14/20] Better support for adjusting config at run-time --- crates/kas-core/src/action.rs | 6 +- crates/kas-core/src/config/config.rs | 119 +++++++++++++++++++------ crates/kas-core/src/config/event.rs | 57 ++++++------ crates/kas-core/src/config/font.rs | 20 +++-- crates/kas-core/src/config/mod.rs | 4 +- crates/kas-core/src/config/options.rs | 4 +- crates/kas-core/src/config/theme.rs | 5 -- crates/kas-core/src/event/cx/config.rs | 2 +- crates/kas-core/src/event/cx/cx_pub.rs | 4 +- crates/kas-widgets/src/event_config.rs | 51 +++++------ 10 files changed, 175 insertions(+), 97 deletions(-) diff --git a/crates/kas-core/src/action.rs b/crates/kas-core/src/action.rs index 4605dacb0..d62ceea35 100644 --- a/crates/kas-core/src/action.rs +++ b/crates/kas-core/src/action.rs @@ -41,13 +41,15 @@ bitflags! { const SET_RECT = 1 << 8; /// Resize all widgets in the window const RESIZE = 1 << 9; - /// Update theme memory + /// Update [`Dimensions`](crate::theme::dimensions::Dimensions) instances /// /// Implies [`Action::RESIZE`]. #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] const THEME_UPDATE = 1 << 10; /// Reload per-window cache of event configuration + /// + /// Implies [`Action::UPDATE`]. #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] const EVENT_CONFIG = 1 << 11; @@ -61,6 +63,8 @@ bitflags! { /// /// *Configuring* widgets assigns [`Id`](crate::Id) identifiers and calls /// [`Events::configure`](crate::Events::configure). + /// + /// Implies [`Action::UPDATE`] since widgets are updated on configure. const RECONFIGURE = 1 << 16; /// Update all widgets /// diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index 2e49df0f8..eaf95df63 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -5,8 +5,7 @@ //! Top-level configuration struct -use super::event::{self, ChangeConfig}; -use super::FontConfig; +use super::{event, FontConfig, FontConfigMsg}; use crate::cast::Cast; use crate::config::Shortcuts; use crate::Action; @@ -16,6 +15,14 @@ use std::cell::{Ref, RefCell}; use std::rc::Rc; use std::time::Duration; +/// A message which may be used to update [`Config`] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ConfigMsg { + Event(event::EventConfigMsg), + Font(FontConfigMsg), +} + /// Base configuration /// /// This is serializable (using `feature = "serde"`) with the following fields: @@ -64,11 +71,6 @@ impl Config { pub fn is_dirty(&self) -> bool { self.is_dirty } - - pub(crate) fn change_config(&mut self, msg: event::ChangeConfig) { - self.event.change_config(msg); - self.is_dirty = true; - } } /// Configuration, adapted for the application and window scale @@ -88,9 +90,7 @@ impl WindowConfig { /// Construct /// /// It is required to call [`Self::update`] before usage. - #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] - #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn new(config: Rc>) -> Self { + pub(crate) fn new(config: Rc>) -> Self { WindowConfig { config, scale_factor: f32::NAN, @@ -103,9 +103,7 @@ impl WindowConfig { } /// Update window-specific/cached values - #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] - #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn update(&mut self, scale_factor: f32) { + pub(crate) fn update(&mut self, scale_factor: f32) { let base = self.config.borrow(); self.scale_factor = scale_factor; self.scroll_flick_sub = base.event.scroll_flick_sub * scale_factor; @@ -115,49 +113,114 @@ impl WindowConfig { self.frame_dur = Duration::from_nanos(base.frame_dur_nanos.cast()); } - /// Borrow access to the [`Config`] - pub fn borrow(&self) -> Ref { + /// Access base (unscaled) [`Config`] + pub fn base(&self) -> Ref { self.config.borrow() } - /// Update event configuration - #[inline] - pub fn change_config(&mut self, msg: ChangeConfig) -> Action { - match self.config.try_borrow_mut() { - Ok(mut config) => { - config.change_config(msg); - Action::EVENT_CONFIG - } - Err(_) => { - log::error!("WindowConfig::change_config: failed to mutably borrow config"); - Action::empty() + /// Update the base config + /// + /// Since it is not known which parts of the configuration are updated, all + /// configuration-update [`Action`]s must be performed. + /// + /// NOTE: adjusting font settings from a running app is not currently + /// supported, excepting font size. + /// + /// NOTE: it is assumed that widget state is not affected by config except + /// (a) state affected by a widget update (e.g. the `EventConfig` widget) + /// and (b) widget size may be affected by font size. + pub fn update_base(&self, f: F) -> Action { + if let Ok(mut c) = self.config.try_borrow_mut() { + c.is_dirty = true; + + let font_size = c.font.size(); + f(&mut c); + + let mut action = Action::EVENT_CONFIG | Action::THEME_UPDATE; + if c.font.size() != font_size { + action |= Action::RESIZE; } + action + } else { + Action::empty() } } -} -impl WindowConfig { /// Access event config pub fn event(&self) -> event::WindowConfig { event::WindowConfig(self) } + /// Update event configuration + pub fn update_event(&self, f: F) -> Action { + if let Ok(mut c) = self.config.try_borrow_mut() { + c.is_dirty = true; + + f(&mut c.event); + Action::EVENT_CONFIG + } else { + Action::empty() + } + } + /// Access font config pub fn font(&self) -> Ref { Ref::map(self.config.borrow(), |c| &c.font) } + /// Set standard font size + /// + /// Units: logical (unscaled) pixels per Em. + /// + /// To convert to Points, multiply by three quarters. + /// + /// NOTE: this is currently the only supported run-time update to font configuration. + pub fn set_font_size(&self, pt_size: f32) -> Action { + if let Ok(mut c) = self.config.try_borrow_mut() { + c.is_dirty = true; + + if pt_size == c.font.size() { + Action::empty() + } else { + c.font.set_size(pt_size); + Action::THEME_UPDATE | Action::UPDATE | Action::RESIZE + } + } else { + Action::empty() + } + } + /// Access shortcut config pub fn shortcuts(&self) -> Ref { Ref::map(self.config.borrow(), |c| &c.shortcuts) } + /// Adjust shortcuts + pub fn update_shortcuts(&self, f: F) -> Action { + if let Ok(mut c) = self.config.try_borrow_mut() { + c.is_dirty = true; + + f(&mut c.shortcuts); + Action::UPDATE + } else { + Action::empty() + } + } + /// Scale factor pub fn scale_factor(&self) -> f32 { debug_assert!(self.scale_factor.is_finite()); self.scale_factor } + /// Update event configuration via a [`ConfigMsg`] + pub fn change_config(&self, msg: ConfigMsg) -> Action { + match msg { + ConfigMsg::Event(msg) => self.update_event(|ev| ev.change_config(msg)), + ConfigMsg::Font(FontConfigMsg::Size(size)) => self.set_font_size(size), + } + } + /// Minimum frame time #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] diff --git a/crates/kas-core/src/config/event.rs b/crates/kas-core/src/config/event.rs index ee30e6653..4a600fa59 100644 --- a/crates/kas-core/src/config/event.rs +++ b/crates/kas-core/src/config/event.rs @@ -10,11 +10,13 @@ use crate::event::ModifiersState; use crate::geom::Offset; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::cell::Ref; use std::time::Duration; -/// Configuration message used to update [`Config`] +/// A message which may be used to update [`Config`] #[derive(Clone, Debug)] -pub enum ChangeConfig { +#[non_exhaustive] +pub enum EventConfigMsg { MenuDelay(u32), TouchSelectDelay(u32), ScrollFlickTimeout(u32), @@ -103,20 +105,20 @@ impl Default for Config { } impl Config { - pub(super) fn change_config(&mut self, msg: ChangeConfig) { + pub(super) fn change_config(&mut self, msg: EventConfigMsg) { match msg { - ChangeConfig::MenuDelay(v) => self.menu_delay_ms = v, - ChangeConfig::TouchSelectDelay(v) => self.touch_select_delay_ms = v, - ChangeConfig::ScrollFlickTimeout(v) => self.scroll_flick_timeout_ms = v, - ChangeConfig::ScrollFlickMul(v) => self.scroll_flick_mul = v, - ChangeConfig::ScrollFlickSub(v) => self.scroll_flick_sub = v, - ChangeConfig::ScrollDistEm(v) => self.scroll_dist_em = v, - ChangeConfig::PanDistThresh(v) => self.pan_dist_thresh = v, - ChangeConfig::MousePan(v) => self.mouse_pan = v, - ChangeConfig::MouseTextPan(v) => self.mouse_text_pan = v, - ChangeConfig::MouseNavFocus(v) => self.mouse_nav_focus = v, - ChangeConfig::TouchNavFocus(v) => self.touch_nav_focus = v, - ChangeConfig::ResetToDefault => *self = Config::default(), + EventConfigMsg::MenuDelay(v) => self.menu_delay_ms = v, + EventConfigMsg::TouchSelectDelay(v) => self.touch_select_delay_ms = v, + EventConfigMsg::ScrollFlickTimeout(v) => self.scroll_flick_timeout_ms = v, + EventConfigMsg::ScrollFlickMul(v) => self.scroll_flick_mul = v, + EventConfigMsg::ScrollFlickSub(v) => self.scroll_flick_sub = v, + EventConfigMsg::ScrollDistEm(v) => self.scroll_dist_em = v, + EventConfigMsg::PanDistThresh(v) => self.pan_dist_thresh = v, + EventConfigMsg::MousePan(v) => self.mouse_pan = v, + EventConfigMsg::MouseTextPan(v) => self.mouse_text_pan = v, + EventConfigMsg::MouseNavFocus(v) => self.mouse_nav_focus = v, + EventConfigMsg::TouchNavFocus(v) => self.touch_nav_focus = v, + EventConfigMsg::ResetToDefault => *self = Config::default(), } } } @@ -129,16 +131,22 @@ impl Config { pub struct WindowConfig<'a>(pub(super) &'a super::WindowConfig); impl<'a> WindowConfig<'a> { + /// Access base (unscaled) event [`Config`] + #[inline] + pub fn base(&self) -> Ref { + Ref::map(self.0.config.borrow(), |c| &c.event) + } + /// Delay before opening/closing menus on mouse hover #[inline] pub fn menu_delay(&self) -> Duration { - Duration::from_millis(self.0.borrow().event.menu_delay_ms.cast()) + Duration::from_millis(self.base().menu_delay_ms.cast()) } /// Delay before switching from panning to (text) selection mode #[inline] pub fn touch_select_delay(&self) -> Duration { - Duration::from_millis(self.0.borrow().event.touch_select_delay_ms.cast()) + Duration::from_millis(self.base().touch_select_delay_ms.cast()) } /// Controls activation of glide/momentum scrolling @@ -148,7 +156,7 @@ impl<'a> WindowConfig<'a> { /// events within this time window are used to calculate the initial speed. #[inline] pub fn scroll_flick_timeout(&self) -> Duration { - Duration::from_millis(self.0.borrow().event.scroll_flick_timeout_ms.cast()) + Duration::from_millis(self.base().scroll_flick_timeout_ms.cast()) } /// Scroll flick velocity decay: `(mul, sub)` @@ -163,10 +171,7 @@ impl<'a> WindowConfig<'a> { /// Units are pixels/second (output is adjusted for the window's scale factor). #[inline] pub fn scroll_flick_decay(&self) -> (f32, f32) { - ( - self.0.borrow().event.scroll_flick_mul, - self.0.scroll_flick_sub, - ) + (self.base().scroll_flick_mul, self.0.scroll_flick_sub) } /// Get distance in pixels to scroll due to mouse wheel @@ -193,25 +198,25 @@ impl<'a> WindowConfig<'a> { /// When to pan general widgets (unhandled events) with the mouse #[inline] pub fn mouse_pan(&self) -> MousePan { - self.0.borrow().event.mouse_pan + self.base().mouse_pan } /// When to pan text fields with the mouse #[inline] pub fn mouse_text_pan(&self) -> MousePan { - self.0.borrow().event.mouse_text_pan + self.base().mouse_text_pan } /// Whether mouse clicks set keyboard navigation focus #[inline] pub fn mouse_nav_focus(&self) -> bool { - self.0.nav_focus && self.0.borrow().event.mouse_nav_focus + self.0.nav_focus && self.base().mouse_nav_focus } /// Whether touchscreen events set keyboard navigation focus #[inline] pub fn touch_nav_focus(&self) -> bool { - self.0.nav_focus && self.0.borrow().event.touch_nav_focus + self.0.nav_focus && self.base().touch_nav_focus } } diff --git a/crates/kas-core/src/config/font.rs b/crates/kas-core/src/config/font.rs index 10e68c18d..0fdfcb506 100644 --- a/crates/kas-core/src/config/font.rs +++ b/crates/kas-core/src/config/font.rs @@ -9,13 +9,23 @@ use crate::text::fonts::{self, AddMode, FontSelector}; use crate::theme::TextClass; use std::collections::BTreeMap; -/// Event handling configuration +/// A message which may be used to update [`FontConfig`] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum FontConfigMsg { + /// Standard font size, in units of pixels-per-Em + Size(f32), +} + +/// Font configuration +/// +/// Note that only changes to [`Self::size`] are currently supported at run-time. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct FontConfig { /// Standard font size, in units of pixels-per-Em #[cfg_attr(feature = "serde", serde(default = "defaults::size"))] - size: f32, + pub size: f32, /// Font aliases, used when searching for a font family matching the key. /// @@ -38,15 +48,15 @@ pub struct FontConfig { /// /// Supported modes: `Prepend`, `Append`, `Replace`. #[cfg_attr(feature = "serde", serde(default))] - aliases: BTreeMap, + pub aliases: BTreeMap, /// Standard fonts #[cfg_attr(feature = "serde", serde(default))] - fonts: BTreeMap>, + pub fonts: BTreeMap>, /// Text glyph rastering settings #[cfg_attr(feature = "serde", serde(default))] - raster: RasterConfig, + pub raster: RasterConfig, } impl Default for FontConfig { diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index 5fd4dd2ab..84be0883c 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -6,12 +6,12 @@ //! Configuration items and utilities mod config; -pub use config::{Config, WindowConfig}; +pub use config::{Config, ConfigMsg, WindowConfig}; pub mod event; mod font; -pub use font::{FontConfig, RasterConfig}; +pub use font::{FontConfig, FontConfigMsg, RasterConfig}; mod format; pub use format::{Error, Format}; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs index 8a87cf232..217ddd640 100644 --- a/crates/kas-core/src/config/options.rs +++ b/crates/kas-core/src/config/options.rs @@ -5,8 +5,8 @@ //! Configuration options -#[cfg(feature = "serde")] use super::{theme, Format}; use super::{Config, Error}; +#[cfg(feature = "serde")] use super::{Format, ThemeConfig}; use crate::draw::DrawSharedImpl; use crate::theme::Theme; #[cfg(feature = "serde")] use crate::util::warn_about_error; @@ -120,7 +120,7 @@ impl Options { match self.config_mode { #[cfg(feature = "serde")] ConfigMode::Read | ConfigMode::ReadWrite if self.theme_config_path.is_file() => { - let config: theme::Config = Format::guess_and_read_path(&self.theme_config_path)?; + let config: ThemeConfig = Format::guess_and_read_path(&self.theme_config_path)?; // Ignore Action: UI isn't built yet let _ = theme.apply_config(&config); } diff --git a/crates/kas-core/src/config/theme.rs b/crates/kas-core/src/config/theme.rs index b7bd9206d..c3f43374d 100644 --- a/crates/kas-core/src/config/theme.rs +++ b/crates/kas-core/src/config/theme.rs @@ -121,11 +121,6 @@ impl ThemeConfig { mod defaults { use super::*; - #[cfg(feature = "serde")] - pub fn add_mode() -> AddMode { - AddMode::Prepend - } - pub fn color_schemes() -> BTreeMap { let mut schemes = BTreeMap::new(); schemes.insert("light".to_string(), ColorsSrgb::light()); diff --git a/crates/kas-core/src/event/cx/config.rs b/crates/kas-core/src/event/cx/config.rs index 8e00f4455..3c80b99b7 100644 --- a/crates/kas-core/src/event/cx/config.rs +++ b/crates/kas-core/src/event/cx/config.rs @@ -126,7 +126,7 @@ impl<'a> ConfigCx<'a> { /// Configure a text object /// /// This selects a font given the [`TextClass`][crate::theme::TextClass], - /// [theme configuration][crate::config::theme::Config] and + /// [theme configuration][crate::config::ThemeConfig] and /// the loaded [fonts][crate::text::fonts]. #[inline] pub fn text_configure(&self, text: &mut Text) { diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index 5cefbc9b9..ef6791037 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -11,7 +11,7 @@ use std::time::Duration; use super::*; use crate::cast::Conv; -use crate::config::event::ChangeConfig; +use crate::config::ConfigMsg; use crate::draw::DrawShared; use crate::geom::{Offset, Vec2}; use crate::theme::{SizeCx, ThemeControl}; @@ -163,7 +163,7 @@ impl EventState { /// Update event configuration #[inline] - pub fn change_config(&mut self, msg: ChangeConfig) { + pub fn change_config(&mut self, msg: ConfigMsg) { self.action |= self.config.change_config(msg); } diff --git a/crates/kas-widgets/src/event_config.rs b/crates/kas-widgets/src/event_config.rs index 2bafe0558..8eb655af7 100644 --- a/crates/kas-widgets/src/event_config.rs +++ b/crates/kas-widgets/src/event_config.rs @@ -6,7 +6,8 @@ //! Drivers for configuration types use crate::{Button, CheckButton, ComboBox, Spinner}; -use kas::config::event::{ChangeConfig, MousePan}; +use kas::config::event::{EventConfigMsg, MousePan}; +use kas::config::ConfigMsg; use kas::prelude::*; impl_scope! { @@ -55,7 +56,7 @@ impl_scope! { (1..3, 10) => self.touch_nav_focus, (0, 11) => "Restore default values:", - (1..3, 11) => Button::label_msg("&Reset", ChangeConfig::ResetToDefault), + (1..3, 11) => Button::label_msg("&Reset", EventConfigMsg::ResetToDefault), }; }] #[impl_default(EventConfig::new())] @@ -88,7 +89,7 @@ impl_scope! { impl Events for Self { fn handle_messages(&mut self, cx: &mut EventCx, _: &()) { if let Some(msg) = cx.try_pop() { - cx.change_config(msg); + cx.change_config(ConfigMsg::Event(msg)); } } } @@ -105,46 +106,46 @@ impl_scope! { EventConfig { core: Default::default(), - menu_delay: Spinner::new(0..=5_000, |cx, _| cx.config().borrow().event.menu_delay_ms) + menu_delay: Spinner::new(0..=5_000, |cx, _| cx.config().base().event.menu_delay_ms) .with_step(50) - .with_msg(ChangeConfig::MenuDelay), - touch_select_delay: Spinner::new(0..=5_000, |cx: &ConfigCx, _| cx.config().borrow().event.touch_select_delay_ms) + .with_msg(EventConfigMsg::MenuDelay), + touch_select_delay: Spinner::new(0..=5_000, |cx: &ConfigCx, _| cx.config().base().event.touch_select_delay_ms) .with_step(50) - .with_msg(ChangeConfig::TouchSelectDelay), - scroll_flick_timeout: Spinner::new(0..=500, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_flick_timeout_ms) + .with_msg(EventConfigMsg::TouchSelectDelay), + scroll_flick_timeout: Spinner::new(0..=500, |cx: &ConfigCx, _| cx.config().base().event.scroll_flick_timeout_ms) .with_step(5) - .with_msg(ChangeConfig::ScrollFlickTimeout), - scroll_flick_mul: Spinner::new(0.0..=1.0, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_flick_mul) + .with_msg(EventConfigMsg::ScrollFlickTimeout), + scroll_flick_mul: Spinner::new(0.0..=1.0, |cx: &ConfigCx, _| cx.config().base().event.scroll_flick_mul) .with_step(0.0625) - .with_msg(ChangeConfig::ScrollFlickMul), - scroll_flick_sub: Spinner::new(0.0..=1.0e4, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_flick_sub) + .with_msg(EventConfigMsg::ScrollFlickMul), + scroll_flick_sub: Spinner::new(0.0..=1.0e4, |cx: &ConfigCx, _| cx.config().base().event.scroll_flick_sub) .with_step(10.0) - .with_msg(ChangeConfig::ScrollFlickSub), - scroll_dist_em: Spinner::new(0.125..=125.0, |cx: &ConfigCx, _| cx.config().borrow().event.scroll_dist_em) + .with_msg(EventConfigMsg::ScrollFlickSub), + scroll_dist_em: Spinner::new(0.125..=125.0, |cx: &ConfigCx, _| cx.config().base().event.scroll_dist_em) .with_step(0.125) - .with_msg(ChangeConfig::ScrollDistEm), - pan_dist_thresh: Spinner::new(0.25..=25.0, |cx: &ConfigCx, _| cx.config().borrow().event.pan_dist_thresh) + .with_msg(EventConfigMsg::ScrollDistEm), + pan_dist_thresh: Spinner::new(0.25..=25.0, |cx: &ConfigCx, _| cx.config().base().event.pan_dist_thresh) .with_step(0.25) - .with_msg(ChangeConfig::PanDistThresh), + .with_msg(EventConfigMsg::PanDistThresh), mouse_pan: ComboBox::new_msg( pan_options, - |cx: &ConfigCx, _| cx.config().borrow().event.mouse_pan, - ChangeConfig::MousePan, + |cx: &ConfigCx, _| cx.config().base().event.mouse_pan, + EventConfigMsg::MousePan, ), mouse_text_pan: ComboBox::new_msg( pan_options, - |cx: &ConfigCx, _| cx.config().borrow().event.mouse_text_pan, - ChangeConfig::MouseTextPan, + |cx: &ConfigCx, _| cx.config().base().event.mouse_text_pan, + EventConfigMsg::MouseTextPan, ), mouse_nav_focus: CheckButton::new_msg( "&Mouse navigation focus", - |cx: &ConfigCx, _| cx.config().borrow().event.mouse_nav_focus, - ChangeConfig::MouseNavFocus, + |cx: &ConfigCx, _| cx.config().base().event.mouse_nav_focus, + EventConfigMsg::MouseNavFocus, ), touch_nav_focus: CheckButton::new_msg( "&Touchscreen navigation focus", - |cx: &ConfigCx, _| cx.config().borrow().event.touch_nav_focus, - ChangeConfig::TouchNavFocus, + |cx: &ConfigCx, _| cx.config().base().event.touch_nav_focus, + EventConfigMsg::TouchNavFocus, ), } } From 8e13338f5430de782e0b94fd9edc8dad9dfa930b Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 30 Apr 2024 17:18:24 +0100 Subject: [PATCH 15/20] Parse theme colours at compile time --- crates/kas-core/src/config/theme.rs | 6 +- crates/kas-core/src/draw/color.rs | 66 ++++++++---- crates/kas-core/src/theme/colors.rs | 123 ++++++++-------------- crates/kas-core/src/theme/simple_theme.rs | 15 ++- 4 files changed, 107 insertions(+), 103 deletions(-) diff --git a/crates/kas-core/src/config/theme.rs b/crates/kas-core/src/config/theme.rs index c3f43374d..6fc3e57e6 100644 --- a/crates/kas-core/src/config/theme.rs +++ b/crates/kas-core/src/config/theme.rs @@ -123,9 +123,9 @@ mod defaults { pub fn color_schemes() -> BTreeMap { let mut schemes = BTreeMap::new(); - schemes.insert("light".to_string(), ColorsSrgb::light()); - schemes.insert("dark".to_string(), ColorsSrgb::dark()); - schemes.insert("blue".to_string(), ColorsSrgb::blue()); + schemes.insert("light".to_string(), ColorsSrgb::LIGHT); + schemes.insert("dark".to_string(), ColorsSrgb::DARK); + schemes.insert("blue".to_string(), ColorsSrgb::BLUE); schemes } diff --git a/crates/kas-core/src/draw/color.rs b/crates/kas-core/src/draw/color.rs index b07a45bd4..022e0aa79 100644 --- a/crates/kas-core/src/draw/color.rs +++ b/crates/kas-core/src/draw/color.rs @@ -281,10 +281,10 @@ impl From<[u8; 4]> for Rgba8Srgb { #[derive(Copy, Clone, Debug, Error)] pub enum ParseError { /// Incorrect input length - #[error("input has unexpected length (expected optional `#` then 6 or 8 bytes")] + #[error("invalid length (expected 6 or 8 bytes")] Length, /// Invalid hex byte - #[error("input byte is not a valid hex byte (expected 0-9, a-f or A-F)")] + #[error("invalid hex byte (expected 0-9, a-f or A-F)")] InvalidHex, } @@ -303,32 +303,54 @@ impl std::str::FromStr for Rgba8Srgb { if s[0] == b'#' { s = &s[1..]; } - if s.len() != 6 && s.len() != 8 { - return Err(ParseError::Length); - } + try_parse_srgb(&s) + } +} - // `val` is copied from the hex crate: - // Copyright (c) 2013-2014 The Rust Project Developers. - // Copyright (c) 2015-2020 The rust-hex Developers. - fn val(c: u8) -> Result { - match c { - b'A'..=b'F' => Ok(c - b'A' + 10), - b'a'..=b'f' => Ok(c - b'a' + 10), - b'0'..=b'9' => Ok(c - b'0'), - _ => Err(ParseError::InvalidHex), - } +/// Compile-time parser for sRGB and sRGBA colours +pub const fn try_parse_srgb(s: &[u8]) -> Result { + if s.len() != 6 && s.len() != 8 { + return Err(ParseError::Length); + } + + // `val` is copied from the hex crate: + // Copyright (c) 2013-2014 The Rust Project Developers. + // Copyright (c) 2015-2020 The rust-hex Developers. + const fn val(c: u8) -> Result { + match c { + b'A'..=b'F' => Ok(c - b'A' + 10), + b'a'..=b'f' => Ok(c - b'a' + 10), + b'0'..=b'9' => Ok(c - b'0'), + _ => Err(()), } + } - fn byte(s: &[u8]) -> Result { - Ok(val(s[0])? << 4 | val(s[1])?) + const fn byte(a: u8, b: u8) -> Result { + match (val(a), val(b)) { + (Ok(hi), Ok(lo)) => Ok(hi << 4 | lo), + _ => Err(()), } + } - let r = byte(&s[0..2])?; - let g = byte(&s[2..4])?; - let b = byte(&s[4..6])?; - let a = if s.len() == 8 { byte(&s[6..8])? } else { 0xFF }; + let r = byte(s[0], s[1]); + let g = byte(s[2], s[3]); + let b = byte(s[4], s[5]); + let a = if s.len() == 8 { byte(s[6], s[7]) } else { Ok(0xFF) }; - Ok(Rgba8Srgb([r, g, b, a])) + match (r, g, b, a) { + (Ok(r), Ok(g), Ok(b), Ok(a)) => Ok(Rgba8Srgb([r, g, b, a])), + _ => Err(ParseError::InvalidHex), + } +} + +/// Compile-time parser for sRGB and sRGBA colours +/// +/// This method has worse diagnostics on error due to limited const- +pub const fn parse_srgb(s: &[u8]) -> Rgba8Srgb { + match try_parse_srgb(s) { + Ok(result) => result, + Err(ParseError::Length) => panic!("invalid length (expected 6 or 8 bytes"), + Err(ParseError::InvalidHex) => panic!("invalid hex byte (expected 0-9, a-f or A-F)"), } } diff --git a/crates/kas-core/src/theme/colors.rs b/crates/kas-core/src/theme/colors.rs index 82369e1b8..5885a6be9 100644 --- a/crates/kas-core/src/theme/colors.rs +++ b/crates/kas-core/src/theme/colors.rs @@ -5,11 +5,10 @@ //! Colour schemes -use crate::draw::color::{Rgba, Rgba8Srgb}; +use crate::draw::color::{parse_srgb, Rgba, Rgba8Srgb}; use crate::event::EventState; use crate::theme::Background; use crate::Id; -use std::str::FromStr; const MULT_DEPRESS: f32 = 0.75; const MULT_HIGHLIGHT: f32 = 1.25; @@ -223,87 +222,57 @@ impl From for ColorsSrgb { } } -impl Default for ColorsLinear { - #[cfg(feature = "dark-light")] - fn default() -> Self { - use dark_light::Mode; - match dark_light::detect() { - Mode::Dark => ColorsSrgb::dark().into(), - Mode::Light | Mode::Default => ColorsSrgb::light().into(), - } - } - - #[cfg(not(feature = "dark-light"))] - #[inline] - fn default() -> Self { - ColorsSrgb::default().into() - } -} - -impl Default for ColorsSrgb { - #[inline] - fn default() -> Self { - ColorsSrgb::light() - } -} - impl ColorsSrgb { /// Default "light" scheme - pub fn light() -> Self { - Colors { - is_dark: false, - background: Rgba8Srgb::from_str("#FAFAFA").unwrap(), - frame: Rgba8Srgb::from_str("#BCBCBC").unwrap(), - accent: Rgba8Srgb::from_str("#8347f2").unwrap(), - accent_soft: Rgba8Srgb::from_str("#B38DF9").unwrap(), - nav_focus: Rgba8Srgb::from_str("#7E3FF2").unwrap(), - edit_bg: Rgba8Srgb::from_str("#FAFAFA").unwrap(), - edit_bg_disabled: Rgba8Srgb::from_str("#DCDCDC").unwrap(), - edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(), - text: Rgba8Srgb::from_str("#000000").unwrap(), - text_invert: Rgba8Srgb::from_str("#FFFFFF").unwrap(), - text_disabled: Rgba8Srgb::from_str("#AAAAAA").unwrap(), - text_sel_bg: Rgba8Srgb::from_str("#A172FA").unwrap(), - } - } + pub const LIGHT: ColorsSrgb = Colors { + is_dark: false, + background: parse_srgb(b"FAFAFA"), + frame: parse_srgb(b"BCBCBC"), + accent: parse_srgb(b"8347f2"), + accent_soft: parse_srgb(b"B38DF9"), + nav_focus: parse_srgb(b"7E3FF2"), + edit_bg: parse_srgb(b"FAFAFA"), + edit_bg_disabled: parse_srgb(b"DCDCDC"), + edit_bg_error: parse_srgb(b"FFBCBC"), + text: parse_srgb(b"000000"), + text_invert: parse_srgb(b"FFFFFF"), + text_disabled: parse_srgb(b"AAAAAA"), + text_sel_bg: parse_srgb(b"A172FA"), + }; /// Dark scheme - pub fn dark() -> Self { - Colors { - is_dark: true, - background: Rgba8Srgb::from_str("#404040").unwrap(), - frame: Rgba8Srgb::from_str("#AAAAAA").unwrap(), - accent: Rgba8Srgb::from_str("#F74C00").unwrap(), - accent_soft: Rgba8Srgb::from_str("#E77346").unwrap(), - nav_focus: Rgba8Srgb::from_str("#D03E00").unwrap(), - edit_bg: Rgba8Srgb::from_str("#303030").unwrap(), - edit_bg_disabled: Rgba8Srgb::from_str("#606060").unwrap(), - edit_bg_error: Rgba8Srgb::from_str("#a06868").unwrap(), - text: Rgba8Srgb::from_str("#FFFFFF").unwrap(), - text_invert: Rgba8Srgb::from_str("#000000").unwrap(), - text_disabled: Rgba8Srgb::from_str("#CBCBCB").unwrap(), - text_sel_bg: Rgba8Srgb::from_str("#E77346").unwrap(), - } - } + pub const DARK: ColorsSrgb = Colors { + is_dark: true, + background: parse_srgb(b"404040"), + frame: parse_srgb(b"AAAAAA"), + accent: parse_srgb(b"F74C00"), + accent_soft: parse_srgb(b"E77346"), + nav_focus: parse_srgb(b"D03E00"), + edit_bg: parse_srgb(b"303030"), + edit_bg_disabled: parse_srgb(b"606060"), + edit_bg_error: parse_srgb(b"a06868"), + text: parse_srgb(b"FFFFFF"), + text_invert: parse_srgb(b"000000"), + text_disabled: parse_srgb(b"CBCBCB"), + text_sel_bg: parse_srgb(b"E77346"), + }; /// Blue scheme - pub fn blue() -> Self { - Colors { - is_dark: false, - background: Rgba8Srgb::from_str("#FFFFFF").unwrap(), - frame: Rgba8Srgb::from_str("#DADADA").unwrap(), - accent: Rgba8Srgb::from_str("#3fafd7").unwrap(), - accent_soft: Rgba8Srgb::from_str("#7CDAFF").unwrap(), - nav_focus: Rgba8Srgb::from_str("#3B697A").unwrap(), - edit_bg: Rgba8Srgb::from_str("#FFFFFF").unwrap(), - edit_bg_disabled: Rgba8Srgb::from_str("#DCDCDC").unwrap(), - edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(), - text: Rgba8Srgb::from_str("#000000").unwrap(), - text_invert: Rgba8Srgb::from_str("#FFFFFF").unwrap(), - text_disabled: Rgba8Srgb::from_str("#AAAAAA").unwrap(), - text_sel_bg: Rgba8Srgb::from_str("#6CC0E1").unwrap(), - } - } + pub const BLUE: ColorsSrgb = Colors { + is_dark: false, + background: parse_srgb(b"FFFFFF"), + frame: parse_srgb(b"DADADA"), + accent: parse_srgb(b"3fafd7"), + accent_soft: parse_srgb(b"7CDAFF"), + nav_focus: parse_srgb(b"3B697A"), + edit_bg: parse_srgb(b"FFFFFF"), + edit_bg_disabled: parse_srgb(b"DCDCDC"), + edit_bg_error: parse_srgb(b"FFBCBC"), + text: parse_srgb(b"000000"), + text_invert: parse_srgb(b"FFFFFF"), + text_disabled: parse_srgb(b"AAAAAA"), + text_sel_bg: parse_srgb(b"6CC0E1"), + }; } impl ColorsLinear { diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 66ea7dfcd..368f5b435 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -24,6 +24,8 @@ use crate::theme::{ColorsLinear, InputState, Theme}; use crate::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; use crate::{Action, Id}; +use super::ColorsSrgb; + /// A simple theme /// /// This theme is functional, but not pretty. It is intended as a template for @@ -46,7 +48,18 @@ impl SimpleTheme { /// Construct #[inline] pub fn new() -> Self { - let cols = ColorsLinear::default(); + #[cfg(not(feature = "dark-light"))] + let cols = ColorsSrgb::LIGHT.into(); + + #[cfg(feature = "dark-light")] + let cols = { + use dark_light::Mode; + match dark_light::detect() { + Mode::Dark => ColorsSrgb::DARK.into(), + Mode::Light | Mode::Default => ColorsSrgb::LIGHT.into(), + } + }; + SimpleTheme { config: Default::default(), cols, From 2f9bcb398d6e29a6cdaa45fc4708e3049befbd9a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 15:31:41 +0100 Subject: [PATCH 16/20] Move ThemeConfig under Config --- crates/kas-core/src/action.rs | 1 + crates/kas-core/src/app/app.rs | 5 +- crates/kas-core/src/app/event_loop.rs | 2 +- crates/kas-core/src/app/shared.rs | 5 +- crates/kas-core/src/app/window.rs | 2 +- crates/kas-core/src/config/config.rs | 25 ++++- crates/kas-core/src/config/mod.rs | 2 +- crates/kas-core/src/config/options.rs | 58 +---------- crates/kas-core/src/config/theme.rs | 112 +++++++++++++--------- crates/kas-core/src/event/cx/cx_pub.rs | 12 ++- crates/kas-core/src/theme/dimensions.rs | 5 +- crates/kas-core/src/theme/flat_theme.rs | 52 ++-------- crates/kas-core/src/theme/multi.rs | 57 +---------- crates/kas-core/src/theme/simple_theme.rs | 80 ++-------------- crates/kas-core/src/theme/theme_dst.rs | 25 +---- crates/kas-core/src/theme/traits.rs | 53 ++-------- crates/kas-wgpu/src/shaded_theme.rs | 51 ++-------- examples/gallery.rs | 52 ++++++---- examples/mandlebrot/mandlebrot.rs | 10 +- examples/stopwatch.rs | 3 +- 20 files changed, 197 insertions(+), 415 deletions(-) diff --git a/crates/kas-core/src/action.rs b/crates/kas-core/src/action.rs index d62ceea35..89def86bb 100644 --- a/crates/kas-core/src/action.rs +++ b/crates/kas-core/src/action.rs @@ -42,6 +42,7 @@ bitflags! { /// Resize all widgets in the window const RESIZE = 1 << 9; /// Update [`Dimensions`](crate::theme::dimensions::Dimensions) instances + /// and theme configuration. /// /// Implies [`Action::RESIZE`]. #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] diff --git a/crates/kas-core/src/app/app.rs b/crates/kas-core/src/app/app.rs index 25ad5d4d2..6c04b92f0 100644 --- a/crates/kas-core/src/app/app.rs +++ b/crates/kas-core/src/app/app.rs @@ -72,10 +72,7 @@ impl_scope! { /// Build with `data` pub fn build(self, data: Data) -> Result> { - let mut theme = self.theme; - let options = self.options.unwrap_or_else(Options::from_env); - options.init_theme_config(&mut theme)?; let config = self.config.unwrap_or_else(|| match options.read_config() { Ok(config) => Rc::new(RefCell::new(config)), @@ -92,7 +89,7 @@ impl_scope! { draw_shared.set_raster_config(config.borrow().font.raster()); let pw = PlatformWrapper(&el); - let state = AppState::new(data, pw, draw_shared, theme, options, config)?; + let state = AppState::new(data, pw, draw_shared, self.theme, options, config)?; Ok(Application { el, diff --git a/crates/kas-core/src/app/event_loop.rs b/crates/kas-core/src/app/event_loop.rs index 2f0694a93..16b03ac45 100644 --- a/crates/kas-core/src/app/event_loop.rs +++ b/crates/kas-core/src/app/event_loop.rs @@ -215,7 +215,7 @@ where elwt.set_control_flow(ControlFlow::Poll); } else { for (_, window) in self.windows.iter_mut() { - window.handle_action(&self.state, action); + window.handle_action(&mut self.state, action); } } } diff --git a/crates/kas-core/src/app/shared.rs b/crates/kas-core/src/app/shared.rs index e2506f743..5504c7a97 100644 --- a/crates/kas-core/src/app/shared.rs +++ b/crates/kas-core/src/app/shared.rs @@ -98,10 +98,7 @@ where } pub(crate) fn on_exit(&self) { - match self - .options - .write_config(&self.shared.config.borrow(), &self.shared.theme) - { + match self.options.write_config(&self.shared.config.borrow()) { Ok(()) => (), Err(error) => warn_about_error("Failed to save config", &error), } diff --git a/crates/kas-core/src/app/window.rs b/crates/kas-core/src/app/window.rs index 5c49b7020..cfb564d5a 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -301,7 +301,7 @@ impl> Window { } /// Handle an action (excludes handling of CLOSE and EXIT) - pub(super) fn handle_action(&mut self, state: &AppState, mut action: Action) { + pub(super) fn handle_action(&mut self, state: &mut AppState, mut action: Action) { if action.contains(Action::EVENT_CONFIG) { if let Some(ref mut window) = self.window { self.ev_state.update_config(window.scale_factor() as f32); diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index eaf95df63..298bbd311 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -5,7 +5,8 @@ //! Top-level configuration struct -use super::{event, FontConfig, FontConfigMsg}; +use super::theme::ThemeConfigMsg; +use super::{event, FontConfig, FontConfigMsg, ThemeConfig}; use crate::cast::Cast; use crate::config::Shortcuts; use crate::Action; @@ -21,6 +22,7 @@ use std::time::Duration; pub enum ConfigMsg { Event(event::EventConfigMsg), Font(FontConfigMsg), + Theme(ThemeConfigMsg), } /// Base configuration @@ -41,6 +43,8 @@ pub struct Config { #[cfg_attr(feature = "serde", serde(default = "Shortcuts::platform_defaults"))] pub shortcuts: Shortcuts, + pub theme: ThemeConfig, + #[cfg_attr(feature = "serde", serde(default = "defaults::frame_dur_nanos"))] frame_dur_nanos: u32, @@ -54,6 +58,7 @@ impl Default for Config { event: event::Config::default(), font: Default::default(), shortcuts: Shortcuts::platform_defaults(), + theme: Default::default(), frame_dur_nanos: defaults::frame_dur_nanos(), is_dirty: false, } @@ -195,6 +200,23 @@ impl WindowConfig { Ref::map(self.config.borrow(), |c| &c.shortcuts) } + /// Access theme config + pub fn theme(&self) -> Ref { + Ref::map(self.config.borrow(), |c| &c.theme) + } + + /// Update theme configuration + pub fn update_theme(&self, f: F) -> Action { + if let Ok(mut c) = self.config.try_borrow_mut() { + c.is_dirty = true; + + f(&mut c.theme); + Action::THEME_UPDATE + } else { + Action::empty() + } + } + /// Adjust shortcuts pub fn update_shortcuts(&self, f: F) -> Action { if let Ok(mut c) = self.config.try_borrow_mut() { @@ -218,6 +240,7 @@ impl WindowConfig { match msg { ConfigMsg::Event(msg) => self.update_event(|ev| ev.change_config(msg)), ConfigMsg::Font(FontConfigMsg::Size(size)) => self.set_font_size(size), + ConfigMsg::Theme(msg) => self.update_theme(|th| th.change_config(msg)), } } diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index 84be0883c..9cc0df65d 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -23,4 +23,4 @@ mod shortcuts; pub use shortcuts::Shortcuts; mod theme; -pub use theme::ThemeConfig; +pub use theme::{ThemeConfig, ThemeConfigMsg}; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs index 217ddd640..d220d9a0b 100644 --- a/crates/kas-core/src/config/options.rs +++ b/crates/kas-core/src/config/options.rs @@ -5,10 +5,8 @@ //! Configuration options +#[cfg(feature = "serde")] use super::Format; use super::{Config, Error}; -#[cfg(feature = "serde")] use super::{Format, ThemeConfig}; -use crate::draw::DrawSharedImpl; -use crate::theme::Theme; #[cfg(feature = "serde")] use crate::util::warn_about_error; use std::env::var; use std::path::PathBuf; @@ -35,8 +33,6 @@ pub enum ConfigMode { pub struct Options { /// Config file path. Default: empty. See `KAS_CONFIG` doc. pub config_path: PathBuf, - /// Theme config path. Default: empty. - pub theme_config_path: PathBuf, /// Config mode. Default: Read. pub config_mode: ConfigMode, } @@ -45,7 +41,6 @@ impl Default for Options { fn default() -> Self { Options { config_path: PathBuf::new(), - theme_config_path: PathBuf::new(), config_mode: ConfigMode::Read, } } @@ -67,12 +62,6 @@ impl Options { /// without reading or writing. This may change to use a platform-specific /// default path in future versions. /// - /// The `KAS_THEME_CONFIG` variable, if given, provides a path to the theme - /// config file, which is read or written according to `KAS_CONFIG_MODE`. - /// If `KAS_THEME_CONFIG` is not specified, platform-default configuration - /// is used without reading or writing. This may change to use a - /// platform-specific default path in future versions. - /// /// The `KAS_CONFIG_MODE` variable determines the read/write mode: /// /// - `Read` (default): read-only @@ -89,10 +78,6 @@ impl Options { options.config_path = v.into(); } - if let Ok(v) = var("KAS_THEME_CONFIG") { - options.theme_config_path = v.into(); - } - if let Ok(mut v) = var("KAS_CONFIG_MODE") { v.make_ascii_uppercase(); options.config_mode = match v.as_str() { @@ -110,37 +95,6 @@ impl Options { options } - /// Load/save and apply theme config on start - /// - /// Requires feature "serde" to load/save config. - pub fn init_theme_config>( - &self, - theme: &mut T, - ) -> Result<(), Error> { - match self.config_mode { - #[cfg(feature = "serde")] - ConfigMode::Read | ConfigMode::ReadWrite if self.theme_config_path.is_file() => { - let config: ThemeConfig = Format::guess_and_read_path(&self.theme_config_path)?; - // Ignore Action: UI isn't built yet - let _ = theme.apply_config(&config); - } - #[cfg(feature = "serde")] - ConfigMode::WriteDefault if !self.theme_config_path.as_os_str().is_empty() => { - let config = theme.config(); - if let Err(error) = - Format::guess_and_write_path(&self.theme_config_path, config.as_ref()) - { - warn_about_error("failed to write default config: ", &error); - } - } - _ => { - let _ = theme; - } - } - - Ok(()) - } - /// Load/save KAS config on start /// /// Requires feature "serde" to load/save config. @@ -169,20 +123,12 @@ impl Options { /// Save all config (on exit or after changes) /// /// Requires feature "serde" to save config. - pub fn write_config>( - &self, - _config: &Config, - _theme: &T, - ) -> Result<(), Error> { + pub fn write_config(&self, _config: &Config) -> Result<(), Error> { #[cfg(feature = "serde")] if self.config_mode == ConfigMode::ReadWrite { if !self.config_path.as_os_str().is_empty() && _config.is_dirty() { Format::guess_and_write_path(&self.config_path, &_config)?; } - let theme_config = _theme.config(); - if !self.theme_config_path.as_os_str().is_empty() && theme_config.is_dirty() { - Format::guess_and_write_path(&self.theme_config_path, theme_config.as_ref())?; - } } Ok(()) diff --git a/crates/kas-core/src/config/theme.rs b/crates/kas-core/src/config/theme.rs index 6fc3e57e6..960039d85 100644 --- a/crates/kas-core/src/config/theme.rs +++ b/crates/kas-core/src/config/theme.rs @@ -6,41 +6,50 @@ //! Theme configuration use crate::theme::ColorsSrgb; -use crate::Action; use std::collections::BTreeMap; use std::time::Duration; +/// A message which may be used to update [`ThemeConfig`] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ThemeConfigMsg { + /// Changes the active colour scheme (only if this already exists) + SetActiveScheme(String), + /// Adds or updates a scheme. Does not change the active scheme. + AddScheme(String, ColorsSrgb), + /// Removes a scheme + RemoveScheme(String), + /// Set the fade duration (ms) + FadeDurationMs(u32), +} + /// Event handling configuration #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ThemeConfig { - #[cfg_attr(feature = "serde", serde(skip))] - dirty: bool, - /// The colour scheme to use #[cfg_attr(feature = "serde", serde(default))] - active_scheme: String, + pub active_scheme: String, /// All colour schemes /// TODO: possibly we should not save default schemes and merge when /// loading (perhaps via a `PartialConfig` type). #[cfg_attr(feature = "serde", serde(default = "defaults::color_schemes"))] - color_schemes: BTreeMap, + pub color_schemes: BTreeMap, /// Text cursor blink rate: delay between switching states #[cfg_attr(feature = "serde", serde(default = "defaults::cursor_blink_rate_ms"))] - cursor_blink_rate_ms: u32, + pub cursor_blink_rate_ms: u32, /// Transition duration used in animations #[cfg_attr(feature = "serde", serde(default = "defaults::transition_fade_ms"))] - transition_fade_ms: u32, + pub transition_fade_ms: u32, } impl Default for ThemeConfig { fn default() -> Self { ThemeConfig { - dirty: false, - active_scheme: Default::default(), + active_scheme: defaults::default_scheme(), color_schemes: defaults::color_schemes(), cursor_blink_rate_ms: defaults::cursor_blink_rate_ms(), transition_fade_ms: defaults::transition_fade_ms(), @@ -48,7 +57,17 @@ impl Default for ThemeConfig { } } -/// Getters +impl ThemeConfig { + pub(super) fn change_config(&mut self, msg: ThemeConfigMsg) { + match msg { + ThemeConfigMsg::SetActiveScheme(scheme) => self.set_active_scheme(scheme), + ThemeConfigMsg::AddScheme(scheme, colors) => self.add_scheme(scheme, colors), + ThemeConfigMsg::RemoveScheme(scheme) => self.remove_scheme(&scheme), + ThemeConfigMsg::FadeDurationMs(dur) => self.transition_fade_ms = dur, + } + } +} + impl ThemeConfig { /// Active colour scheme (name) /// @@ -58,24 +77,44 @@ impl ThemeConfig { &self.active_scheme } + /// Set the active colour scheme (by name) + /// + /// Does nothing if the named scheme is not found. + pub fn set_active_scheme(&mut self, scheme: impl ToString) { + let scheme = scheme.to_string(); + if self.color_schemes.keys().any(|k| *k == scheme) { + self.active_scheme = scheme.to_string(); + } + } + /// Iterate over all colour schemes #[inline] - pub fn color_schemes_iter(&self) -> impl Iterator { + pub fn color_schemes(&self) -> impl Iterator { self.color_schemes.iter().map(|(s, t)| (s.as_str(), t)) } /// Get a colour scheme by name #[inline] - pub fn get_color_scheme(&self, name: &str) -> Option { - self.color_schemes.get(name).cloned() + pub fn get_color_scheme(&self, name: &str) -> Option<&ColorsSrgb> { + self.color_schemes.get(name) } /// Get the active colour scheme - /// - /// Even this one isn't guaranteed to exist. #[inline] - pub fn get_active_scheme(&self) -> Option { - self.color_schemes.get(&self.active_scheme).cloned() + pub fn get_active_scheme(&self) -> &ColorsSrgb { + self.color_schemes + .get(&self.active_scheme) + .unwrap_or(&ColorsSrgb::LIGHT) + } + + /// Add or update a colour scheme + pub fn add_scheme(&mut self, scheme: impl ToString, colors: ColorsSrgb) { + self.color_schemes.insert(scheme.to_string(), colors); + } + + /// Remove a colour scheme + pub fn remove_scheme(&mut self, scheme: &str) { + self.color_schemes.remove(scheme); } /// Get the cursor blink rate (delay) @@ -91,35 +130,22 @@ impl ThemeConfig { } } -/// Setters -impl ThemeConfig { - /// Set colour scheme - pub fn set_active_scheme(&mut self, scheme: impl ToString) { - self.dirty = true; - self.active_scheme = scheme.to_string(); - } -} - -/// Other functions -impl ThemeConfig { - /// Currently this is just "set". Later, maybe some type of merge. - #[allow(clippy::float_cmp)] - pub fn apply_config(&mut self, other: &ThemeConfig) -> Action { - let action = if self != other { Action::REDRAW } else { Action::empty() }; +mod defaults { + use super::*; - *self = other.clone(); - action + #[cfg(not(feature = "dark-light"))] + pub fn default_scheme() -> String { + "light".to_string() } - #[cfg(feature = "serde")] - #[inline] - pub(crate) fn is_dirty(&self) -> bool { - self.dirty + #[cfg(feature = "dark-light")] + pub fn default_scheme() -> String { + use dark_light::Mode; + match dark_light::detect() { + Mode::Dark => "dark".to_string(), + Mode::Light | Mode::Default => "light".to_string(), + } } -} - -mod defaults { - use super::*; pub fn color_schemes() -> BTreeMap { let mut schemes = BTreeMap::new(); diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index ef6791037..f66c1f94f 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -255,7 +255,7 @@ impl EventState { self.action |= Action::EXIT; } - /// Notify that a [`Action`] action should happen + /// Notify that an [`Action`] should happen /// /// This causes the given action to happen after event handling. /// @@ -288,6 +288,16 @@ impl EventState { } } + /// Notify that an [`Action`] should happen for the whole window + /// + /// Using [`Self::action`] with a widget `id` instead of this method is + /// potentially more efficient (supports future optimisations), but not + /// always possible. + #[inline] + pub fn window_action(&mut self, action: Action) { + self.action |= action; + } + /// Attempts to set a fallback to receive [`Event::Command`] /// /// In case a navigation key is pressed (see [`Command`]) but no widget has diff --git a/crates/kas-core/src/theme/dimensions.rs b/crates/kas-core/src/theme/dimensions.rs index 92c69d3fa..d1636ba94 100644 --- a/crates/kas-core/src/theme/dimensions.rs +++ b/crates/kas-core/src/theme/dimensions.rs @@ -13,7 +13,7 @@ use std::rc::Rc; use super::anim::AnimState; use super::{Feature, FrameStyle, MarginStyle, MarkStyle, SizableText, TextClass, ThemeSize}; use crate::cast::traits::*; -use crate::config::{ThemeConfig, WindowConfig}; +use crate::config::WindowConfig; use crate::dir::Directional; use crate::geom::{Rect, Size, Vec2}; use crate::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules, Stretch}; @@ -160,13 +160,12 @@ impl Window { pub fn new( dims: &Parameters, config: &WindowConfig, - theme_config: &ThemeConfig, fonts: Rc>, ) -> Self { Window { dims: Dimensions::new(dims, config), fonts, - anim: AnimState::new(theme_config), + anim: AnimState::new(&config.theme()), } } diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index 6b055116f..118d003bb 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -11,7 +11,7 @@ use std::time::Instant; use super::SimpleTheme; use crate::cast::traits::*; -use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::config::{Config, WindowConfig}; use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; @@ -21,7 +21,7 @@ use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; use crate::theme::{ColorsLinear, InputState, Theme}; use crate::theme::{ThemeControl, ThemeDraw, ThemeSize}; -use crate::{Action, Id}; +use crate::Id; // Used to ensure a rectangular background is inside a circular corner. // Also the maximum inner radius of circular borders to overlap with this rect. @@ -61,16 +61,6 @@ impl FlatTheme { let base = SimpleTheme::new(); FlatTheme { base } } - - /// Set the colour scheme - /// - /// If no scheme by this name is found the scheme is left unchanged. - #[inline] - #[must_use] - pub fn with_colours(mut self, scheme: &str) -> Self { - self.base = self.base.with_colours(scheme); - self - } } fn dimensions() -> dim::Parameters { @@ -99,24 +89,18 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { - >::config(&self.base) - } - - fn apply_config(&mut self, config: &ThemeConfig) -> Action { - >::apply_config(&mut self.base, config) - } - fn init(&mut self, config: &Config) { >::init(&mut self.base, config) } - fn new_window(&self, config: &WindowConfig) -> Self::Window { + fn new_window(&mut self, config: &WindowConfig) -> Self::Window { + self.base.cols = config.theme().get_active_scheme().into(); let fonts = self.base.fonts.as_ref().unwrap().clone(); - dim::Window::new(&dimensions(), config, &self.base.config, fonts) + dim::Window::new(&dimensions(), config, fonts) } - fn update_window(&self, w: &mut Self::Window, config: &WindowConfig) { + fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) { + self.base.cols = config.theme().get_active_scheme().into(); w.update(&dimensions(), config); } @@ -150,27 +134,7 @@ where } } -impl ThemeControl for FlatTheme { - fn active_scheme(&self) -> &str { - self.base.active_scheme() - } - - fn list_schemes(&self) -> Vec<&str> { - self.base.list_schemes() - } - - fn get_scheme(&self, name: &str) -> Option<&super::ColorsSrgb> { - self.base.get_scheme(name) - } - - fn get_colors(&self) -> &ColorsLinear { - self.base.get_colors() - } - - fn set_colors(&mut self, name: String, cols: ColorsLinear) -> Action { - self.base.set_colors(name, cols) - } -} +impl ThemeControl for FlatTheme {} impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where diff --git a/crates/kas-core/src/theme/multi.rs b/crates/kas-core/src/theme/multi.rs index 25ff0deda..f99028ed2 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -5,14 +5,13 @@ //! Wrapper around mutliple themes, supporting run-time switching -use std::collections::HashMap; - use super::{ColorsLinear, Theme, ThemeDst, Window}; -use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::config::{Config, WindowConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; use crate::Action; +use std::collections::HashMap; type DynTheme = Box>; @@ -82,29 +81,17 @@ impl Theme for MultiTheme { type Window = Box; type Draw<'a> = Box; - fn config(&self) -> std::borrow::Cow { - self.themes[self.active].config() - } - - fn apply_config(&mut self, config: &ThemeConfig) -> Action { - let mut action = Action::empty(); - for theme in &mut self.themes { - action |= theme.apply_config(config); - } - action - } - fn init(&mut self, config: &Config) { for theme in &mut self.themes { theme.init(config); } } - fn new_window(&self, config: &WindowConfig) -> Self::Window { + fn new_window(&mut self, config: &WindowConfig) -> Self::Window { self.themes[self.active].new_window(config) } - fn update_window(&self, window: &mut Self::Window, config: &WindowConfig) { + fn update_window(&mut self, window: &mut Self::Window, config: &WindowConfig) { self.themes[self.active].update_window(window, config); } @@ -132,42 +119,6 @@ impl Theme for MultiTheme { } impl ThemeControl for MultiTheme { - fn active_scheme(&self) -> &str { - self.themes[self.active].active_scheme() - } - - fn set_scheme(&mut self, scheme: &str) -> Action { - // Slightly inefficient, but sufficient: update all - // (Otherwise we would have to call set_scheme in set_theme too.) - let mut action = Action::empty(); - for theme in &mut self.themes { - action |= theme.set_scheme(scheme); - } - action - } - - fn list_schemes(&self) -> Vec<&str> { - // We list only schemes of the active theme. Probably all themes should - // have the same schemes anyway. - self.themes[self.active].list_schemes() - } - - fn get_scheme(&self, name: &str) -> Option<&super::ColorsSrgb> { - self.themes[self.active].get_scheme(name) - } - - fn get_colors(&self) -> &ColorsLinear { - self.themes[self.active].get_colors() - } - - fn set_colors(&mut self, name: String, cols: ColorsLinear) -> Action { - let mut action = Action::empty(); - for theme in &mut self.themes { - action |= theme.set_colors(name.clone(), cols.clone()); - } - action - } - fn set_theme(&mut self, theme: &str) -> Action { if let Some(index) = self.names.get(theme).cloned() { if index != self.active { diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 368f5b435..a348a8e45 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -12,7 +12,7 @@ use std::rc::Rc; use std::time::Instant; use crate::cast::traits::*; -use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::config::{Config, WindowConfig}; use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; @@ -22,7 +22,7 @@ use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; use crate::theme::{ColorsLinear, InputState, Theme}; use crate::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; -use crate::{Action, Id}; +use crate::Id; use super::ColorsSrgb; @@ -32,7 +32,6 @@ use super::ColorsSrgb; /// other themes. #[derive(Clone, Debug)] pub struct SimpleTheme { - pub config: ThemeConfig, pub cols: ColorsLinear, dims: dim::Parameters, pub fonts: Option>>, @@ -48,35 +47,12 @@ impl SimpleTheme { /// Construct #[inline] pub fn new() -> Self { - #[cfg(not(feature = "dark-light"))] - let cols = ColorsSrgb::LIGHT.into(); - - #[cfg(feature = "dark-light")] - let cols = { - use dark_light::Mode; - match dark_light::detect() { - Mode::Dark => ColorsSrgb::DARK.into(), - Mode::Light | Mode::Default => ColorsSrgb::LIGHT.into(), - } - }; - SimpleTheme { - config: Default::default(), - cols, + cols: ColorsSrgb::LIGHT.into(), // value is unimportant dims: Default::default(), fonts: None, } } - - /// Set the colour scheme - /// - /// If no scheme by this name is found the scheme is left unchanged. - #[inline] - #[must_use] - pub fn with_colours(mut self, name: &str) -> Self { - let _ = self.set_scheme(name); - self - } } pub struct DrawHandle<'a, DS: DrawSharedImpl> { @@ -93,19 +69,6 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { - std::borrow::Cow::Borrowed(&self.config) - } - - fn apply_config(&mut self, config: &ThemeConfig) -> Action { - let mut action = self.config.apply_config(config); - if let Some(cols) = self.config.get_active_scheme() { - self.cols = cols.into(); - action |= Action::REDRAW; - } - action - } - fn init(&mut self, config: &Config) { let fonts = fonts::library(); if let Err(e) = fonts.select_default() { @@ -120,12 +83,14 @@ where )); } - fn new_window(&self, config: &WindowConfig) -> Self::Window { + fn new_window(&mut self, config: &WindowConfig) -> Self::Window { + self.cols = config.theme().get_active_scheme().into(); let fonts = self.fonts.as_ref().unwrap().clone(); - dim::Window::new(&self.dims, config, &self.config, fonts) + dim::Window::new(&self.dims, config, fonts) } - fn update_window(&self, w: &mut Self::Window, config: &WindowConfig) { + fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) { + self.cols = config.theme().get_active_scheme().into(); w.update(&self.dims, config); } @@ -159,34 +124,7 @@ where } } -impl ThemeControl for SimpleTheme { - fn active_scheme(&self) -> &str { - self.config.active_scheme() - } - - fn list_schemes(&self) -> Vec<&str> { - self.config - .color_schemes_iter() - .map(|(name, _)| name) - .collect() - } - - fn get_scheme(&self, name: &str) -> Option<&super::ColorsSrgb> { - self.config - .color_schemes_iter() - .find_map(|item| (name == item.0).then_some(item.1)) - } - - fn get_colors(&self) -> &ColorsLinear { - &self.cols - } - - fn set_colors(&mut self, name: String, cols: ColorsLinear) -> Action { - self.config.set_active_scheme(name); - self.cols = cols; - Action::REDRAW - } -} +impl ThemeControl for SimpleTheme {} impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where diff --git a/crates/kas-core/src/theme/theme_dst.rs b/crates/kas-core/src/theme/theme_dst.rs index e3edca10e..3a63a2f70 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -6,11 +6,10 @@ //! Stack-DST versions of theme traits use super::{Theme, Window}; -use crate::config::{Config, ThemeConfig, WindowConfig}; +use crate::config::{Config, WindowConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; use crate::theme::{ThemeControl, ThemeDraw}; -use crate::Action; /// As [`Theme`], but without associated types /// @@ -18,12 +17,6 @@ use crate::Action; /// [`Theme`]. It is intended only for use where a less parameterised /// trait is required. pub trait ThemeDst: ThemeControl { - /// Get current configuration - fn config(&self) -> std::borrow::Cow; - - /// Apply/set the passed config - fn apply_config(&mut self, config: &ThemeConfig) -> Action; - /// Theme initialisation /// /// See also [`Theme::init`]. @@ -32,12 +25,12 @@ pub trait ThemeDst: ThemeControl { /// Construct per-window storage /// /// See also [`Theme::new_window`]. - fn new_window(&self, config: &WindowConfig) -> Box; + fn new_window(&mut self, config: &WindowConfig) -> Box; /// Update a window created by [`Theme::new_window`] /// /// See also [`Theme::update_window`]. - fn update_window(&self, window: &mut dyn Window, config: &WindowConfig); + fn update_window(&mut self, window: &mut dyn Window, config: &WindowConfig); fn draw<'a>( &'a self, @@ -53,24 +46,16 @@ pub trait ThemeDst: ThemeControl { } impl> ThemeDst for T { - fn config(&self) -> std::borrow::Cow { - self.config() - } - - fn apply_config(&mut self, config: &ThemeConfig) -> Action { - self.apply_config(config) - } - fn init(&mut self, config: &Config) { self.init(config); } - fn new_window(&self, config: &WindowConfig) -> Box { + fn new_window(&mut self, config: &WindowConfig) -> Box { let window = >::new_window(self, config); Box::new(window) } - fn update_window(&self, window: &mut dyn Window, config: &WindowConfig) { + fn update_window(&mut self, window: &mut dyn Window, config: &WindowConfig) { let window = window.as_any_mut().downcast_mut().unwrap(); self.update_window(window, config); } diff --git a/crates/kas-core/src/theme/traits.rs b/crates/kas-core/src/theme/traits.rs index f28dd320e..6371c7212 100644 --- a/crates/kas-core/src/theme/traits.rs +++ b/crates/kas-core/src/theme/traits.rs @@ -5,8 +5,8 @@ //! Theme traits -use super::{ColorsLinear, ColorsSrgb, ThemeDraw, ThemeSize}; -use crate::config::{Config, ThemeConfig, WindowConfig}; +use super::{ColorsLinear, ThemeDraw, ThemeSize}; +use crate::config::{Config, WindowConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; use crate::{autoimpl, Action}; @@ -16,43 +16,11 @@ use std::any::Any; /// Interface through which a theme can be adjusted at run-time /// -/// All methods return a [`Action`] to enable correct action when a theme +/// All methods return an [`Action`] to enable correct action when a theme /// is updated via [`EventCx::adjust_theme`]. When adjusting a theme before /// the UI is started, this return value can be safely ignored. #[crate::autoimpl(for &mut T, Box)] pub trait ThemeControl { - /// Get the name of the active color scheme - fn active_scheme(&self) -> &str; - - /// List available color schemes - fn list_schemes(&self) -> Vec<&str>; - - /// Get colors of a named scheme - fn get_scheme(&self, name: &str) -> Option<&ColorsSrgb>; - - /// Access the in-use color scheme - fn get_colors(&self) -> &ColorsLinear; - - /// Set colors directly - /// - /// This may be used to provide a custom color scheme. The `name` is - /// compulsary (and returned by [`Self::active_scheme`]). - /// The `name` is also used when saving config, though the custom colors are - /// not currently saved in this config. - fn set_colors(&mut self, name: String, scheme: ColorsLinear) -> Action; - - /// Change the color scheme - /// - /// If no scheme by this name is found the scheme is left unchanged. - fn set_scheme(&mut self, name: &str) -> Action { - if name != self.active_scheme() { - if let Some(scheme) = self.get_scheme(name) { - return self.set_colors(name.to_string(), scheme.into()); - } - } - Action::empty() - } - /// Switch the theme /// /// Most themes do not react to this method; [`super::MultiTheme`] uses @@ -79,12 +47,6 @@ pub trait Theme: ThemeControl { DS: 'a, Self: 'a; - /// Get current configuration - fn config(&self) -> std::borrow::Cow; - - /// Apply/set the passed config - fn apply_config(&mut self, config: &ThemeConfig) -> Action; - /// Theme initialisation /// /// The toolkit must call this method before [`Theme::new_window`] @@ -96,6 +58,9 @@ pub trait Theme: ThemeControl { /// Construct per-window storage /// + /// Updates theme from configuration and constructs a scaled per-window size + /// cache. + /// /// On "standard" monitors, the `dpi_factor` is 1. High-DPI screens may /// have a factor of 2 or higher. The factor may not be an integer; e.g. /// `9/8 = 1.125` works well with many 1440p screens. It is recommended to @@ -105,12 +70,12 @@ pub trait Theme: ThemeControl { /// ``` /// /// A reference to the draw backend is provided allowing configuration. - fn new_window(&self, config: &WindowConfig) -> Self::Window; + fn new_window(&mut self, config: &WindowConfig) -> Self::Window; /// Update a window created by [`Theme::new_window`] /// - /// This is called when the DPI factor changes or theme dimensions change. - fn update_window(&self, window: &mut Self::Window, config: &WindowConfig); + /// This is called when the DPI factor changes or theme config or dimensions change. + fn update_window(&mut self, window: &mut Self::Window, config: &WindowConfig); /// Prepare to draw and construct a [`ThemeDraw`] object /// diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index 9f5f64b09..0a6e706b3 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -11,7 +11,7 @@ use std::time::Instant; use crate::{DrawShaded, DrawShadedImpl}; use kas::cast::traits::*; -use kas::config::{Config, ThemeConfig, WindowConfig}; +use kas::config::{Config, WindowConfig}; use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::event::EventState; @@ -21,7 +21,7 @@ use kas::theme::dimensions as dim; use kas::theme::{Background, ThemeControl, ThemeDraw, ThemeSize}; use kas::theme::{ColorsLinear, FlatTheme, InputState, SimpleTheme, Theme}; use kas::theme::{FrameStyle, MarkStyle, TextClass}; -use kas::{Action, Id}; +use kas::Id; /// A theme using simple shading to give apparent depth to elements #[derive(Clone, Debug)] @@ -41,15 +41,6 @@ impl ShadedTheme { let base = SimpleTheme::new(); ShadedTheme { base } } - - /// Set the colour scheme - /// - /// If no scheme by this name is found the scheme is left unchanged. - #[must_use] - pub fn with_colours(mut self, scheme: &str) -> Self { - self.base = self.base.with_colours(scheme); - self - } } fn dimensions() -> dim::Parameters { @@ -87,24 +78,18 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn config(&self) -> std::borrow::Cow { - >::config(&self.base) - } - - fn apply_config(&mut self, config: &ThemeConfig) -> Action { - >::apply_config(&mut self.base, config) - } - fn init(&mut self, config: &Config) { >::init(&mut self.base, config) } - fn new_window(&self, config: &WindowConfig) -> Self::Window { + fn new_window(&mut self, config: &WindowConfig) -> Self::Window { + self.base.cols = config.theme().get_active_scheme().into(); let fonts = self.base.fonts.as_ref().unwrap().clone(); - dim::Window::new(&dimensions(), config, &self.base.config, fonts) + dim::Window::new(&dimensions(), config, fonts) } - fn update_window(&self, w: &mut Self::Window, config: &WindowConfig) { + fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) { + self.base.cols = config.theme().get_active_scheme().into(); w.update(&dimensions(), config); } @@ -138,27 +123,7 @@ where } } -impl ThemeControl for ShadedTheme { - fn active_scheme(&self) -> &str { - self.base.active_scheme() - } - - fn list_schemes(&self) -> Vec<&str> { - self.base.list_schemes() - } - - fn get_scheme(&self, name: &str) -> Option<&kas::theme::ColorsSrgb> { - self.base.get_scheme(name) - } - - fn get_colors(&self) -> &ColorsLinear { - self.base.get_colors() - } - - fn set_colors(&mut self, name: String, cols: ColorsLinear) -> Action { - self.base.set_colors(name, cols) - } -} +impl ThemeControl for ShadedTheme {} impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where diff --git a/examples/gallery.rs b/examples/gallery.rs index 2a498cf32..c41c5d026 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -9,11 +9,12 @@ //! (excepting custom graphics). use kas::collection; +use kas::config::{ConfigMsg, ThemeConfigMsg}; use kas::dir::Right; use kas::event::Key; use kas::prelude::*; use kas::resvg::Svg; -use kas::theme::{MarginStyle, ThemeControl}; +use kas::theme::MarginStyle; use kas::widgets::{column, *}; #[derive(Debug, Default)] @@ -59,7 +60,6 @@ fn widgets() -> Box> { #[derive(Clone, Debug)] enum Item { Button, - Theme(&'static str), Check(bool), Combo(Entry), Radio(u32), @@ -162,15 +162,24 @@ fn widgets() -> Box> { row![ "Button (image)", row![ - Button::new_msg(img_light.clone(), Item::Theme("light")) - .with_color("#B38DF9".parse().unwrap()) - .with_access_key(Key::Character("h".into())), - Button::new_msg(img_light, Item::Theme("blue")) - .with_color("#7CDAFF".parse().unwrap()) - .with_access_key(Key::Character("b".into())), - Button::new_msg(img_dark, Item::Theme("dark")) - .with_color("#E77346".parse().unwrap()) - .with_access_key(Key::Character("k".into())), + Button::new_msg( + img_light.clone(), + ConfigMsg::Theme(ThemeConfigMsg::SetActiveScheme("light".to_string())) + ) + .with_color("#B38DF9".parse().unwrap()) + .with_access_key(Key::Character("h".into())), + Button::new_msg( + img_light, + ConfigMsg::Theme(ThemeConfigMsg::SetActiveScheme("blue".to_string())) + ) + .with_color("#7CDAFF".parse().unwrap()) + .with_access_key(Key::Character("b".into())), + Button::new_msg( + img_dark, + ConfigMsg::Theme(ThemeConfigMsg::SetActiveScheme("dark".to_string())) + ) + .with_color("#E77346".parse().unwrap()) + .with_access_key(Key::Character("k".into())), ] .map_any() .pack(AlignHints::CENTER), @@ -233,7 +242,7 @@ fn widgets() -> Box> { println!("ScrollMsg({value})"); data.ratio = value as f32 / 100.0; }) - .on_message(|cx, data, item| { + .on_message(|_, data, item| { println!("Message: {item:?}"); match item { Item::Check(v) => data.check = v, @@ -242,10 +251,14 @@ fn widgets() -> Box> { Item::Spinner(value) | Item::Slider(value) => { data.value = value; } - Item::Theme(name) => cx.adjust_theme(|theme| theme.set_scheme(name)), Item::Text(text) => data.text = text, _ => (), } + }) + .on_message(|cx, _, msg| { + println!("Message: {msg:?}"); + let act = cx.config().change_config(msg); + cx.window_action(act); }); let ui = adapt::AdaptEvents::new(ui) @@ -552,13 +565,11 @@ fn main() -> kas::app::Result<()> { }) .menu("&Style", |menu| { menu.submenu("&Colours", |mut menu| { - // Enumerate colour schemes. Access through the app since - // this handles config loading. - for name in app.theme().list_schemes().iter() { + // Enumerate colour schemes. + for (name, _) in app.config().theme.color_schemes() { let mut title = String::with_capacity(name.len() + 1); match name { - &"" => title.push_str("&Default"), - &"dark" => title.push_str("Dar&k"), + "dark" => title.push_str("Dar&k"), name => { let mut iter = name.char_indices(); if let Some((_, c)) = iter.next() { @@ -604,7 +615,10 @@ fn main() -> kas::app::Result<()> { } Menu::Colour(name) => { println!("Colour scheme: {name:?}"); - cx.adjust_theme(|theme| theme.set_scheme(&name)); + let act = cx + .config() + .update_theme(|theme| theme.set_active_scheme(name)); + cx.window_action(act); } Menu::Disabled(disabled) => { state.disabled = disabled; diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs index 89c0c2def..5aef7a24e 100644 --- a/examples/mandlebrot/mandlebrot.rs +++ b/examples/mandlebrot/mandlebrot.rs @@ -484,10 +484,10 @@ fn main() -> kas::app::Result<()> { env_logger::init(); let window = Window::new(MandlebrotUI::new(), "Mandlebrot"); - let theme = kas::theme::FlatTheme::new().with_colours("dark"); - kas::app::WgpuBuilder::new(PipeBuilder) + let theme = kas::theme::FlatTheme::new(); + let mut app = kas::app::WgpuBuilder::new(PipeBuilder) .with_theme(theme) - .build(())? - .with(window) - .run() + .build(())?; + app.config_mut().theme.set_active_scheme("dark"); + app.with(window).run() } diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index d82a84b01..e6f64a710 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -59,8 +59,9 @@ fn main() -> kas::app::Result<()> { .with_transparent(true) .with_restrictions(true, true); - let theme = kas_wgpu::ShadedTheme::new().with_colours("dark"); + let theme = kas_wgpu::ShadedTheme::new(); let mut app = kas::app::Default::with_theme(theme).build(())?; app.config_mut().font.set_size(24.0); + app.config_mut().theme.set_active_scheme("dark"); app.with(window).run() } From ed14a4e959b1ea8b1e257d7f03954535cb3d401b Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 16:26:17 +0100 Subject: [PATCH 17/20] Add active_theme to ThemeConfig; remove ThemeControl Also make ThemeConfig's setters return an Action --- crates/kas-core/src/app/shared.rs | 17 +------- crates/kas-core/src/config/config.rs | 7 ++-- crates/kas-core/src/config/theme.rs | 47 ++++++++++++++++++++--- crates/kas-core/src/event/cx/cx_pub.rs | 8 +--- crates/kas-core/src/theme/flat_theme.rs | 7 ++-- crates/kas-core/src/theme/mod.rs | 2 +- crates/kas-core/src/theme/multi.rs | 35 +++++++++-------- crates/kas-core/src/theme/simple_theme.rs | 8 ++-- crates/kas-core/src/theme/theme_dst.rs | 10 +++-- crates/kas-core/src/theme/traits.rs | 23 ++--------- crates/kas-wgpu/src/shaded_theme.rs | 7 ++-- examples/gallery.rs | 5 ++- examples/mandlebrot/mandlebrot.rs | 2 +- examples/stopwatch.rs | 2 +- 14 files changed, 94 insertions(+), 86 deletions(-) diff --git a/crates/kas-core/src/app/shared.rs b/crates/kas-core/src/app/shared.rs index 5504c7a97..8ecff4aa0 100644 --- a/crates/kas-core/src/app/shared.rs +++ b/crates/kas-core/src/app/shared.rs @@ -8,7 +8,7 @@ use super::{AppData, AppGraphicsBuilder, Error, Pending, Platform}; use crate::config::{Config, Options}; use crate::draw::DrawShared; -use crate::theme::{Theme, ThemeControl}; +use crate::theme::Theme; use crate::util::warn_about_error; use crate::{draw, messages::MessageStack, Action, WindowId}; use std::any::TypeId; @@ -56,7 +56,7 @@ where ) -> Result { let platform = pw.platform(); let draw = kas::draw::SharedState::new(draw_shared); - theme.init(&*config.borrow()); + theme.init(&config); #[cfg(feature = "clipboard")] let clipboard = match Clipboard::new() { @@ -189,14 +189,6 @@ pub(crate) trait AppShared { /// clipboard support. fn set_primary(&mut self, content: String); - /// Adjust the theme - /// - /// Note: theme adjustments apply to all windows, as does the [`Action`] - /// returned from the closure. - // - // TODO(opt): pass f by value, not boxed - fn adjust_theme<'s>(&'s mut self, f: Box Action + 's>); - /// Access the [`DrawShared`] object fn draw_shared(&mut self) -> &mut dyn DrawShared; @@ -300,11 +292,6 @@ impl> AppShared } } - fn adjust_theme<'s>(&'s mut self, f: Box Action + 's>) { - let action = f(&mut self.theme); - self.pending.push_back(Pending::Action(action)); - } - fn draw_shared(&mut self) -> &mut dyn DrawShared { &mut self.draw } diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index 298bbd311..aaadd94be 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -206,12 +206,11 @@ impl WindowConfig { } /// Update theme configuration - pub fn update_theme(&self, f: F) -> Action { + pub fn update_theme Action>(&self, f: F) -> Action { if let Ok(mut c) = self.config.try_borrow_mut() { c.is_dirty = true; - f(&mut c.theme); - Action::THEME_UPDATE + f(&mut c.theme) } else { Action::empty() } @@ -240,7 +239,7 @@ impl WindowConfig { match msg { ConfigMsg::Event(msg) => self.update_event(|ev| ev.change_config(msg)), ConfigMsg::Font(FontConfigMsg::Size(size)) => self.set_font_size(size), - ConfigMsg::Theme(msg) => self.update_theme(|th| th.change_config(msg)), + ConfigMsg::Theme(msg) => self.update_theme(|theme| theme.change_config(msg)), } } diff --git a/crates/kas-core/src/config/theme.rs b/crates/kas-core/src/config/theme.rs index 960039d85..b731fa2b8 100644 --- a/crates/kas-core/src/config/theme.rs +++ b/crates/kas-core/src/config/theme.rs @@ -6,6 +6,7 @@ //! Theme configuration use crate::theme::ColorsSrgb; +use crate::Action; use std::collections::BTreeMap; use std::time::Duration; @@ -13,6 +14,8 @@ use std::time::Duration; #[derive(Clone, Debug)] #[non_exhaustive] pub enum ThemeConfigMsg { + /// Changes the active theme (reliant on `MultiTheme` to do the work) + SetActiveTheme(String), /// Changes the active colour scheme (only if this already exists) SetActiveScheme(String), /// Adds or updates a scheme. Does not change the active scheme. @@ -27,8 +30,12 @@ pub enum ThemeConfigMsg { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ThemeConfig { - /// The colour scheme to use + /// The theme to use (used by `MultiTheme`) #[cfg_attr(feature = "serde", serde(default))] + pub active_theme: String, + + /// The colour scheme to use + #[cfg_attr(feature = "serde", serde(default = "defaults::default_scheme"))] pub active_scheme: String, /// All colour schemes @@ -49,6 +56,7 @@ pub struct ThemeConfig { impl Default for ThemeConfig { fn default() -> Self { ThemeConfig { + active_theme: "".to_string(), active_scheme: defaults::default_scheme(), color_schemes: defaults::color_schemes(), cursor_blink_rate_ms: defaults::cursor_blink_rate_ms(), @@ -58,17 +66,35 @@ impl Default for ThemeConfig { } impl ThemeConfig { - pub(super) fn change_config(&mut self, msg: ThemeConfigMsg) { + pub(super) fn change_config(&mut self, msg: ThemeConfigMsg) -> Action { match msg { + ThemeConfigMsg::SetActiveTheme(theme) => self.set_active_theme(theme), ThemeConfigMsg::SetActiveScheme(scheme) => self.set_active_scheme(scheme), ThemeConfigMsg::AddScheme(scheme, colors) => self.add_scheme(scheme, colors), ThemeConfigMsg::RemoveScheme(scheme) => self.remove_scheme(&scheme), - ThemeConfigMsg::FadeDurationMs(dur) => self.transition_fade_ms = dur, + ThemeConfigMsg::FadeDurationMs(dur) => { + self.transition_fade_ms = dur; + Action::empty() + } } } } impl ThemeConfig { + /// Set the active theme (by name) + /// + /// Only does anything if `MultiTheme` (or another multiplexer) is in use + /// and knows this theme. + pub fn set_active_theme(&mut self, theme: impl ToString) -> Action { + let theme = theme.to_string(); + if self.active_theme == theme { + Action::empty() + } else { + self.active_theme = theme; + Action::THEME_SWITCH + } + } + /// Active colour scheme (name) /// /// An empty string will resolve the default colour scheme. @@ -80,10 +106,13 @@ impl ThemeConfig { /// Set the active colour scheme (by name) /// /// Does nothing if the named scheme is not found. - pub fn set_active_scheme(&mut self, scheme: impl ToString) { + pub fn set_active_scheme(&mut self, scheme: impl ToString) -> Action { let scheme = scheme.to_string(); if self.color_schemes.keys().any(|k| *k == scheme) { self.active_scheme = scheme.to_string(); + Action::THEME_UPDATE + } else { + Action::empty() } } @@ -108,13 +137,19 @@ impl ThemeConfig { } /// Add or update a colour scheme - pub fn add_scheme(&mut self, scheme: impl ToString, colors: ColorsSrgb) { + pub fn add_scheme(&mut self, scheme: impl ToString, colors: ColorsSrgb) -> Action { self.color_schemes.insert(scheme.to_string(), colors); + Action::empty() } /// Remove a colour scheme - pub fn remove_scheme(&mut self, scheme: &str) { + pub fn remove_scheme(&mut self, scheme: &str) -> Action { self.color_schemes.remove(scheme); + if scheme == self.active_scheme { + Action::THEME_UPDATE + } else { + Action::empty() + } } /// Get the cursor blink rate (delay) diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index f66c1f94f..9293cad37 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -14,7 +14,7 @@ use crate::cast::Conv; use crate::config::ConfigMsg; use crate::draw::DrawShared; use crate::geom::{Offset, Vec2}; -use crate::theme::{SizeCx, ThemeControl}; +use crate::theme::SizeCx; #[cfg(all(wayland_platform, feature = "clipboard"))] use crate::util::warn_about_error; #[allow(unused)] use crate::{Events, Layout}; // for doc-links @@ -953,12 +953,6 @@ impl<'a> EventCx<'a> { self.shared.set_primary(content) } - /// Adjust the theme - #[inline] - pub fn adjust_theme Action>(&mut self, f: F) { - self.shared.adjust_theme(Box::new(f)); - } - /// Get a [`SizeCx`] /// /// Warning: sizes are calculated using the window's current scale factor. diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index 118d003bb..eedca0590 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -5,6 +5,7 @@ //! Flat theme +use std::cell::RefCell; use std::f32; use std::ops::Range; use std::time::Instant; @@ -20,7 +21,7 @@ use crate::text::TextDisplay; use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; use crate::theme::{ColorsLinear, InputState, Theme}; -use crate::theme::{ThemeControl, ThemeDraw, ThemeSize}; +use crate::theme::{ThemeDraw, ThemeSize}; use crate::Id; // Used to ensure a rectangular background is inside a circular corner. @@ -89,7 +90,7 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn init(&mut self, config: &Config) { + fn init(&mut self, config: &RefCell) { >::init(&mut self.base, config) } @@ -134,8 +135,6 @@ where } } -impl ThemeControl for FlatTheme {} - impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where DS::Draw: DrawRoundedImpl, diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs index ca50099d7..e40814572 100644 --- a/crates/kas-core/src/theme/mod.rs +++ b/crates/kas-core/src/theme/mod.rs @@ -37,6 +37,6 @@ pub use size::SizeCx; pub use style::*; pub use text::{SizableText, Text}; pub use theme_dst::ThemeDst; -pub use traits::{Theme, ThemeControl, Window}; +pub use traits::{Theme, Window}; #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] pub use {draw::ThemeDraw, size::ThemeSize}; diff --git a/crates/kas-core/src/theme/multi.rs b/crates/kas-core/src/theme/multi.rs index f99028ed2..f57438acb 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -9,8 +9,8 @@ use super::{ColorsLinear, Theme, ThemeDst, Window}; use crate::config::{Config, WindowConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; -use crate::theme::{ThemeControl, ThemeDraw}; -use crate::Action; +use crate::theme::ThemeDraw; +use std::cell::RefCell; use std::collections::HashMap; type DynTheme = Box>; @@ -81,13 +81,30 @@ impl Theme for MultiTheme { type Window = Box; type Draw<'a> = Box; - fn init(&mut self, config: &Config) { + fn init(&mut self, config: &RefCell) { + if config.borrow().theme.active_theme.is_empty() { + for (name, index) in &self.names { + if *index == self.active { + let _ = config.borrow_mut().theme.set_active_theme(name.to_string()); + break; + } + } + } + for theme in &mut self.themes { theme.init(config); } } fn new_window(&mut self, config: &WindowConfig) -> Self::Window { + // We may switch themes here + let theme = &config.theme().active_theme; + if let Some(index) = self.names.get(theme).cloned() { + if index != self.active { + self.active = index; + } + } + self.themes[self.active].new_window(config) } @@ -117,15 +134,3 @@ impl Theme for MultiTheme { self.themes[self.active].clear_color() } } - -impl ThemeControl for MultiTheme { - fn set_theme(&mut self, theme: &str) -> Action { - if let Some(index) = self.names.get(theme).cloned() { - if index != self.active { - self.active = index; - return Action::THEME_SWITCH; - } - } - Action::empty() - } -} diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index a348a8e45..94fb4edf6 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -6,6 +6,7 @@ //! Simple theme use linear_map::LinearMap; +use std::cell::RefCell; use std::f32; use std::ops::Range; use std::rc::Rc; @@ -21,7 +22,7 @@ use crate::text::{fonts, Effect, TextDisplay}; use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle, TextClass}; use crate::theme::{ColorsLinear, InputState, Theme}; -use crate::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; +use crate::theme::{SelectionStyle, ThemeDraw, ThemeSize}; use crate::Id; use super::ColorsSrgb; @@ -69,13 +70,14 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn init(&mut self, config: &Config) { + fn init(&mut self, config: &RefCell) { let fonts = fonts::library(); if let Err(e) = fonts.select_default() { panic!("Error loading font: {e}"); } self.fonts = Some(Rc::new( config + .borrow() .font .iter_fonts() .filter_map(|(c, s)| fonts.select_font(s).ok().map(|id| (*c, id))) @@ -124,8 +126,6 @@ where } } -impl ThemeControl for SimpleTheme {} - impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where DS::Draw: DrawRoundedImpl, diff --git a/crates/kas-core/src/theme/theme_dst.rs b/crates/kas-core/src/theme/theme_dst.rs index 3a63a2f70..bb059436d 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -5,22 +5,24 @@ //! Stack-DST versions of theme traits +use std::cell::RefCell; + use super::{Theme, Window}; use crate::config::{Config, WindowConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; -use crate::theme::{ThemeControl, ThemeDraw}; +use crate::theme::ThemeDraw; /// As [`Theme`], but without associated types /// /// This trait is implemented automatically for all implementations of /// [`Theme`]. It is intended only for use where a less parameterised /// trait is required. -pub trait ThemeDst: ThemeControl { +pub trait ThemeDst { /// Theme initialisation /// /// See also [`Theme::init`]. - fn init(&mut self, config: &Config); + fn init(&mut self, config: &RefCell); /// Construct per-window storage /// @@ -46,7 +48,7 @@ pub trait ThemeDst: ThemeControl { } impl> ThemeDst for T { - fn init(&mut self, config: &Config) { + fn init(&mut self, config: &RefCell) { self.init(config); } diff --git a/crates/kas-core/src/theme/traits.rs b/crates/kas-core/src/theme/traits.rs index 6371c7212..4eb618a96 100644 --- a/crates/kas-core/src/theme/traits.rs +++ b/crates/kas-core/src/theme/traits.rs @@ -6,30 +6,15 @@ //! Theme traits use super::{ColorsLinear, ThemeDraw, ThemeSize}; +use crate::autoimpl; use crate::config::{Config, WindowConfig}; use crate::draw::{color, DrawIface, DrawSharedImpl}; use crate::event::EventState; -use crate::{autoimpl, Action}; use std::any::Any; +use std::cell::RefCell; #[allow(unused)] use crate::event::EventCx; -/// Interface through which a theme can be adjusted at run-time -/// -/// All methods return an [`Action`] to enable correct action when a theme -/// is updated via [`EventCx::adjust_theme`]. When adjusting a theme before -/// the UI is started, this return value can be safely ignored. -#[crate::autoimpl(for &mut T, Box)] -pub trait ThemeControl { - /// Switch the theme - /// - /// Most themes do not react to this method; [`super::MultiTheme`] uses - /// it to switch themes. - fn set_theme(&mut self, _theme: &str) -> Action { - Action::empty() - } -} - /// A *theme* provides widget sizing and drawing implementations. /// /// The theme is generic over some `DrawIface`. @@ -37,7 +22,7 @@ pub trait ThemeControl { /// Objects of this type are copied within each window's data structure. For /// large resources (e.g. fonts and icons) consider using external storage. #[autoimpl(for Box)] -pub trait Theme: ThemeControl { +pub trait Theme { /// The associated [`Window`] implementation. type Window: Window; @@ -54,7 +39,7 @@ pub trait Theme: ThemeControl { /// /// At a minimum, a theme must load a font to [`crate::text::fonts`]. /// The first font loaded (by any theme) becomes the default font. - fn init(&mut self, config: &Config); + fn init(&mut self, config: &RefCell); /// Construct per-window storage /// diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index 0a6e706b3..fe3ceb8b3 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -5,6 +5,7 @@ //! Shaded theme +use std::cell::RefCell; use std::f32; use std::ops::Range; use std::time::Instant; @@ -18,7 +19,7 @@ use kas::event::EventState; use kas::geom::*; use kas::text::TextDisplay; use kas::theme::dimensions as dim; -use kas::theme::{Background, ThemeControl, ThemeDraw, ThemeSize}; +use kas::theme::{Background, ThemeDraw, ThemeSize}; use kas::theme::{ColorsLinear, FlatTheme, InputState, SimpleTheme, Theme}; use kas::theme::{FrameStyle, MarkStyle, TextClass}; use kas::Id; @@ -78,7 +79,7 @@ where type Window = dim::Window; type Draw<'a> = DrawHandle<'a, DS>; - fn init(&mut self, config: &Config) { + fn init(&mut self, config: &RefCell) { >::init(&mut self.base, config) } @@ -123,8 +124,6 @@ where } } -impl ThemeControl for ShadedTheme {} - impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where DS::Draw: DrawRoundedImpl + DrawShadedImpl, diff --git a/examples/gallery.rs b/examples/gallery.rs index c41c5d026..e1ecdb3b3 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -611,7 +611,10 @@ fn main() -> kas::app::Result<()> { let ui = Adapt::new(ui, AppData::default()).on_message(|cx, state, msg| match msg { Menu::Theme(name) => { println!("Theme: {name:?}"); - cx.adjust_theme(|theme| theme.set_theme(name)); + let act = cx + .config() + .update_theme(|theme| theme.set_active_theme(name)); + cx.window_action(act); } Menu::Colour(name) => { println!("Colour scheme: {name:?}"); diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs index 5aef7a24e..457f50f44 100644 --- a/examples/mandlebrot/mandlebrot.rs +++ b/examples/mandlebrot/mandlebrot.rs @@ -488,6 +488,6 @@ fn main() -> kas::app::Result<()> { let mut app = kas::app::WgpuBuilder::new(PipeBuilder) .with_theme(theme) .build(())?; - app.config_mut().theme.set_active_scheme("dark"); + let _ = app.config_mut().theme.set_active_scheme("dark"); app.with(window).run() } diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index e6f64a710..4e01aa4aa 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -62,6 +62,6 @@ fn main() -> kas::app::Result<()> { let theme = kas_wgpu::ShadedTheme::new(); let mut app = kas::app::Default::with_theme(theme).build(())?; app.config_mut().font.set_size(24.0); - app.config_mut().theme.set_active_scheme("dark"); + let _ = app.config_mut().theme.set_active_scheme("dark"); app.with(window).run() } From e90bca6938703b9e59931ff17b69ee8207be3bf7 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 16:34:19 +0100 Subject: [PATCH 18/20] Let event and font config setters also return an Action --- crates/kas-core/src/action.rs | 6 ++++-- crates/kas-core/src/config/config.rs | 14 +++----------- crates/kas-core/src/config/event.rs | 5 ++++- crates/kas-core/src/config/font.rs | 10 ++++++++-- examples/calculator.rs | 2 +- examples/counter.rs | 2 +- examples/stopwatch.rs | 2 +- examples/sync-counter.rs | 2 +- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/crates/kas-core/src/action.rs b/crates/kas-core/src/action.rs index 89def86bb..e21ce8e26 100644 --- a/crates/kas-core/src/action.rs +++ b/crates/kas-core/src/action.rs @@ -12,8 +12,10 @@ bitflags! { /// while others don't reqiure a context but do require that some *action* /// is performed afterwards. This enum is used to convey that action. /// - /// An `Action` should be passed to a context: `cx.action(self.id(), action)` - /// (assuming `self` is a widget). + /// An `Action` produced at run-time should be passed to a context: + /// `cx.action(self.id(), action)` (assuming `self` is a widget). + /// An `Action` produced before starting the GUI may be discarded, for + /// example: `let _ = app.config_mut().font.set_size(24.0);`. /// /// Two `Action` values may be combined via bit-or (`a | b`). #[must_use] diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index aaadd94be..a7361726e 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -157,12 +157,10 @@ impl WindowConfig { } /// Update event configuration - pub fn update_event(&self, f: F) -> Action { + pub fn update_event Action>(&self, f: F) -> Action { if let Ok(mut c) = self.config.try_borrow_mut() { c.is_dirty = true; - - f(&mut c.event); - Action::EVENT_CONFIG + f(&mut c.event) } else { Action::empty() } @@ -183,13 +181,7 @@ impl WindowConfig { pub fn set_font_size(&self, pt_size: f32) -> Action { if let Ok(mut c) = self.config.try_borrow_mut() { c.is_dirty = true; - - if pt_size == c.font.size() { - Action::empty() - } else { - c.font.set_size(pt_size); - Action::THEME_UPDATE | Action::UPDATE | Action::RESIZE - } + c.font.set_size(pt_size) } else { Action::empty() } diff --git a/crates/kas-core/src/config/event.rs b/crates/kas-core/src/config/event.rs index 4a600fa59..63e905d86 100644 --- a/crates/kas-core/src/config/event.rs +++ b/crates/kas-core/src/config/event.rs @@ -8,6 +8,7 @@ use crate::cast::{Cast, CastFloat}; use crate::event::ModifiersState; use crate::geom::Offset; +use crate::Action; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::cell::Ref; @@ -105,7 +106,7 @@ impl Default for Config { } impl Config { - pub(super) fn change_config(&mut self, msg: EventConfigMsg) { + pub(super) fn change_config(&mut self, msg: EventConfigMsg) -> Action { match msg { EventConfigMsg::MenuDelay(v) => self.menu_delay_ms = v, EventConfigMsg::TouchSelectDelay(v) => self.touch_select_delay_ms = v, @@ -120,6 +121,8 @@ impl Config { EventConfigMsg::TouchNavFocus(v) => self.touch_nav_focus = v, EventConfigMsg::ResetToDefault => *self = Config::default(), } + + Action::EVENT_CONFIG } } diff --git a/crates/kas-core/src/config/font.rs b/crates/kas-core/src/config/font.rs index 0fdfcb506..34f8faf7d 100644 --- a/crates/kas-core/src/config/font.rs +++ b/crates/kas-core/src/config/font.rs @@ -7,6 +7,7 @@ use crate::text::fonts::{self, AddMode, FontSelector}; use crate::theme::TextClass; +use crate::Action; use std::collections::BTreeMap; /// A message which may be used to update [`FontConfig`] @@ -150,8 +151,13 @@ impl FontConfig { /// Units: logical (unscaled) pixels per Em. /// /// To convert to Points, multiply by three quarters. - pub fn set_size(&mut self, pt_size: f32) { - self.size = pt_size; + pub fn set_size(&mut self, pt_size: f32) -> Action { + if self.size != pt_size { + self.size = pt_size; + Action::THEME_UPDATE | Action::UPDATE + } else { + Action::empty() + } } } diff --git a/examples/calculator.rs b/examples/calculator.rs index ea6990e27..f5d934370 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -74,7 +74,7 @@ fn main() -> kas::app::Result<()> { let theme = kas_wgpu::ShadedTheme::new(); let mut app = kas::app::Default::with_theme(theme).build(())?; - app.config_mut().font.set_size(24.0); + let _ = app.config_mut().font.set_size(24.0); app.with(calc_ui()).run() } diff --git a/examples/counter.rs b/examples/counter.rs index da8acfe16..8fbefc54a 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -29,6 +29,6 @@ fn main() -> kas::app::Result<()> { let theme = kas::theme::SimpleTheme::new(); let mut app = kas::app::Default::with_theme(theme).build(())?; - app.config_mut().font.set_size(24.0); + let _ = app.config_mut().font.set_size(24.0); app.with(Window::new(counter(), "Counter")).run() } diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 4e01aa4aa..d3fcb6a7b 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -61,7 +61,7 @@ fn main() -> kas::app::Result<()> { let theme = kas_wgpu::ShadedTheme::new(); let mut app = kas::app::Default::with_theme(theme).build(())?; - app.config_mut().font.set_size(24.0); + let _ = app.config_mut().font.set_size(24.0); let _ = app.config_mut().theme.set_active_scheme("dark"); app.with(window).run() } diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs index 26737021e..3538e175d 100644 --- a/examples/sync-counter.rs +++ b/examples/sync-counter.rs @@ -59,7 +59,7 @@ fn main() -> kas::app::Result<()> { let theme = kas_wgpu::ShadedTheme::new(); let mut app = kas::app::Default::with_theme(theme).build(count)?; - app.config_mut().font.set_size(24.0); + let _ = app.config_mut().font.set_size(24.0); app.with(counter("Counter 1")) .with(counter("Counter 2")) .run() From fb97ed8cac82605c0b153db1f96a1406bd89f44a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 16:40:58 +0100 Subject: [PATCH 19/20] Make kas_core::config::event module private --- crates/kas-core/src/config/config.rs | 22 ++++++++++++---------- crates/kas-core/src/config/event.rs | 22 +++++++++++----------- crates/kas-core/src/config/mod.rs | 3 ++- crates/kas-widgets/src/event_config.rs | 3 +-- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index a7361726e..bacf99463 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -5,8 +5,8 @@ //! Top-level configuration struct -use super::theme::ThemeConfigMsg; -use super::{event, FontConfig, FontConfigMsg, ThemeConfig}; +use super::{EventConfig, EventConfigMsg, EventWindowConfig}; +use super::{FontConfig, FontConfigMsg, ThemeConfig, ThemeConfigMsg}; use crate::cast::Cast; use crate::config::Shortcuts; use crate::Action; @@ -20,7 +20,7 @@ use std::time::Duration; #[derive(Clone, Debug)] #[non_exhaustive] pub enum ConfigMsg { - Event(event::EventConfigMsg), + Event(EventConfigMsg), Font(FontConfigMsg), Theme(ThemeConfigMsg), } @@ -29,14 +29,16 @@ pub enum ConfigMsg { /// /// This is serializable (using `feature = "serde"`) with the following fields: /// -/// > `event`: [`event::Config`] \ -/// > `shortcuts`: [`Shortcuts`] +/// > `event`: [`EventConfig`] \ +/// > `font`: [`FontConfig`] \ +/// > `shortcuts`: [`Shortcuts`] \ +/// > `theme`: [`ThemeConfig`] /// /// For descriptions of configuration effects, see [`WindowConfig`] methods. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Config { - pub event: event::Config, + pub event: EventConfig, pub font: FontConfig, @@ -55,7 +57,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Config { - event: event::Config::default(), + event: EventConfig::default(), font: Default::default(), shortcuts: Shortcuts::platform_defaults(), theme: Default::default(), @@ -152,12 +154,12 @@ impl WindowConfig { } /// Access event config - pub fn event(&self) -> event::WindowConfig { - event::WindowConfig(self) + pub fn event(&self) -> EventWindowConfig { + EventWindowConfig(self) } /// Update event configuration - pub fn update_event Action>(&self, f: F) -> Action { + pub fn update_event Action>(&self, f: F) -> Action { if let Ok(mut c) = self.config.try_borrow_mut() { c.is_dirty = true; f(&mut c.event) diff --git a/crates/kas-core/src/config/event.rs b/crates/kas-core/src/config/event.rs index 63e905d86..e6436cfe7 100644 --- a/crates/kas-core/src/config/event.rs +++ b/crates/kas-core/src/config/event.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::cell::Ref; use std::time::Duration; -/// A message which may be used to update [`Config`] +/// A message which may be used to update [`EventConfig`] #[derive(Clone, Debug)] #[non_exhaustive] pub enum EventConfigMsg { @@ -48,10 +48,10 @@ pub enum EventConfigMsg { /// > `mouse_nav_focus`: `bool` \ /// > `touch_nav_focus`: `bool` /// -/// For descriptions of configuration effects, see [`WindowConfig`] methods. +/// For descriptions of configuration effects, see [`EventWindowConfig`] methods. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Config { +pub struct EventConfig { #[cfg_attr(feature = "serde", serde(default = "defaults::menu_delay_ms"))] pub menu_delay_ms: u32, @@ -87,9 +87,9 @@ pub struct Config { pub touch_nav_focus: bool, } -impl Default for Config { +impl Default for EventConfig { fn default() -> Self { - Config { + EventConfig { menu_delay_ms: defaults::menu_delay_ms(), touch_select_delay_ms: defaults::touch_select_delay_ms(), scroll_flick_timeout_ms: defaults::scroll_flick_timeout_ms(), @@ -105,7 +105,7 @@ impl Default for Config { } } -impl Config { +impl EventConfig { pub(super) fn change_config(&mut self, msg: EventConfigMsg) -> Action { match msg { EventConfigMsg::MenuDelay(v) => self.menu_delay_ms = v, @@ -119,7 +119,7 @@ impl Config { EventConfigMsg::MouseTextPan(v) => self.mouse_text_pan = v, EventConfigMsg::MouseNavFocus(v) => self.mouse_nav_focus = v, EventConfigMsg::TouchNavFocus(v) => self.touch_nav_focus = v, - EventConfigMsg::ResetToDefault => *self = Config::default(), + EventConfigMsg::ResetToDefault => *self = EventConfig::default(), } Action::EVENT_CONFIG @@ -131,12 +131,12 @@ impl Config { /// This is a helper to read event configuration, adapted for the current /// application and window scale. #[derive(Clone, Debug)] -pub struct WindowConfig<'a>(pub(super) &'a super::WindowConfig); +pub struct EventWindowConfig<'a>(pub(super) &'a super::WindowConfig); -impl<'a> WindowConfig<'a> { - /// Access base (unscaled) event [`Config`] +impl<'a> EventWindowConfig<'a> { + /// Access base (unscaled) event [`EventConfig`] #[inline] - pub fn base(&self) -> Ref { + pub fn base(&self) -> Ref { Ref::map(self.0.config.borrow(), |c| &c.event) } diff --git a/crates/kas-core/src/config/mod.rs b/crates/kas-core/src/config/mod.rs index 9cc0df65d..a84c1773d 100644 --- a/crates/kas-core/src/config/mod.rs +++ b/crates/kas-core/src/config/mod.rs @@ -8,7 +8,8 @@ mod config; pub use config::{Config, ConfigMsg, WindowConfig}; -pub mod event; +mod event; +pub use event::{EventConfig, EventConfigMsg, EventWindowConfig, MousePan}; mod font; pub use font::{FontConfig, FontConfigMsg, RasterConfig}; diff --git a/crates/kas-widgets/src/event_config.rs b/crates/kas-widgets/src/event_config.rs index 8eb655af7..7a708c6a8 100644 --- a/crates/kas-widgets/src/event_config.rs +++ b/crates/kas-widgets/src/event_config.rs @@ -6,8 +6,7 @@ //! Drivers for configuration types use crate::{Button, CheckButton, ComboBox, Spinner}; -use kas::config::event::{EventConfigMsg, MousePan}; -use kas::config::ConfigMsg; +use kas::config::{ConfigMsg, EventConfigMsg, MousePan}; use kas::prelude::*; impl_scope! { From 546d08ec306c35ee15572f7573d25bf9679d4dd7 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 1 May 2024 17:02:35 +0100 Subject: [PATCH 20/20] Replace Config::frame_dur_nanos with max_fps --- crates/kas-core/src/config/config.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/kas-core/src/config/config.rs b/crates/kas-core/src/config/config.rs index bacf99463..f3e0a3053 100644 --- a/crates/kas-core/src/config/config.rs +++ b/crates/kas-core/src/config/config.rs @@ -32,7 +32,8 @@ pub enum ConfigMsg { /// > `event`: [`EventConfig`] \ /// > `font`: [`FontConfig`] \ /// > `shortcuts`: [`Shortcuts`] \ -/// > `theme`: [`ThemeConfig`] +/// > `theme`: [`ThemeConfig`] \ +/// > `max_fps`: `u32` /// /// For descriptions of configuration effects, see [`WindowConfig`] methods. #[derive(Clone, Debug, PartialEq)] @@ -47,8 +48,12 @@ pub struct Config { pub theme: ThemeConfig, - #[cfg_attr(feature = "serde", serde(default = "defaults::frame_dur_nanos"))] - frame_dur_nanos: u32, + /// FPS limiter + /// + /// This forces a minimum delay between frames to limit the frame rate. + /// `0` is treated specially as no delay. + #[cfg_attr(feature = "serde", serde(default = "defaults::max_fps"))] + pub max_fps: u32, #[cfg_attr(feature = "serde", serde(skip))] is_dirty: bool, @@ -61,7 +66,7 @@ impl Default for Config { font: Default::default(), shortcuts: Shortcuts::platform_defaults(), theme: Default::default(), - frame_dur_nanos: defaults::frame_dur_nanos(), + max_fps: defaults::max_fps(), is_dirty: false, } } @@ -117,7 +122,12 @@ impl WindowConfig { let dpem = base.font.size() * scale_factor; self.scroll_dist = base.event.scroll_dist_em * dpem; self.pan_dist_thresh = base.event.pan_dist_thresh * scale_factor; - self.frame_dur = Duration::from_nanos(base.frame_dur_nanos.cast()); + let dur_ns = if base.max_fps == 0 { + 0 + } else { + 1_000_000_000 / base.max_fps.max(1) + }; + self.frame_dur = Duration::from_nanos(dur_ns.cast()); } /// Access base (unscaled) [`Config`] @@ -247,7 +257,7 @@ impl WindowConfig { } mod defaults { - pub fn frame_dur_nanos() -> u32 { - 12_500_000 // 1e9 / 80 + pub fn max_fps() -> u32 { + 80 } }