diff --git a/crates/kas-core/src/action.rs b/crates/kas-core/src/action.rs index 7681a9034..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] @@ -41,18 +43,31 @@ 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 + /// and theme configuration. + /// + /// 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; + /// 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 /// [`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/app/app.rs b/crates/kas-core/src/app/app.rs index eed45f472..6c04b92f0 100644 --- a/crates/kas-core/src/app/app.rs +++ b/crates/kas-core/src/app/app.rs @@ -6,13 +6,12 @@ //! [`Application`] and supporting elements use super::{AppData, AppGraphicsBuilder, AppState, Platform, ProxyAction, Result}; -use crate::config::Options; +use crate::config::{Config, Options}; use crate::draw::{DrawShared, DrawSharedImpl}; -use crate::event; -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::RefCell; +use std::cell::{Ref, RefCell, RefMut}; use std::rc::Rc; use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy}; @@ -27,7 +26,7 @@ impl_scope! { graphical: G, theme: T, options: Option, - config: Option>>, + config: Option>>, } impl Self { @@ -54,29 +53,26 @@ 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 } /// 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)), @@ -85,14 +81,15 @@ 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)?; + let state = AppState::new(data, pw, draw_shared, self.theme, options, config)?; Ok(Application { el, @@ -166,6 +163,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 { diff --git a/crates/kas-core/src/app/event_loop.rs b/crates/kas-core/src/app/event_loop.rs index c76dea882..16b03ac45 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}; @@ -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/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 b144437f2..8ecff4aa0 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::{Config, Options}; +use crate::draw::DrawShared; +use crate::theme::Theme; +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; @@ -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,11 +52,11 @@ 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); - theme.init(&mut draw); + let draw = kas::draw::SharedState::new(draw_shared); + theme.init(&config); #[cfg(feature = "clipboard")] let clipboard = match Clipboard::new() { @@ -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), } @@ -192,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; @@ -303,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/app/window.rs b/crates/kas-core/src/app/window.rs index cd99b81bf..cfb564d5a 100644 --- a/crates/kas-core/src/app/window.rs +++ b/crates/kas-core/src/app/window.rs @@ -8,14 +8,14 @@ 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::config::WindowConfig; +use crate::draw::{color::Rgba, AnimationState, DrawSharedImpl}; +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 _}; +use crate::{autoimpl, messages::MessageStack, Action, Id, Layout, LayoutExt, Widget, WindowId}; use std::mem::take; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -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. @@ -298,12 +301,10 @@ 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 { - 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; } } @@ -312,14 +313,21 @@ 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 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; } if action.contains(Action::RESIZE) { if let Some(ref mut window) = self.window { diff --git a/crates/kas-core/src/config.rs b/crates/kas-core/src/config.rs deleted file mode 100644 index 4da8e7956..000000000 --- a/crates/kas-core/src/config.rs +++ /dev/null @@ -1,377 +0,0 @@ -// 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 read/write utilities - -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 -/// -/// See [`Options::from_env`] documentation. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum ConfigMode { - /// Read-only mode - Read, - /// Read-write mode - /// - /// This mode reads config on start and writes changes on exit. - ReadWrite, - /// Use default config and write out - /// - /// This mode only writes initial (default) config and does not update. - 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 { - /// 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, -} - -impl Default for Options { - fn default() -> Self { - Options { - config_path: PathBuf::new(), - theme_config_path: PathBuf::new(), - config_mode: ConfigMode::Read, - } - } -} - -impl Options { - /// Construct a new instance, reading from environment variables - /// - /// The following environment variables are read, in case-insensitive mode. - /// - /// # Config files - /// - /// WARNING: file formats are not stable and may not be compatible across - /// KAS versions (aside from patch versions)! - /// - /// The `KAS_CONFIG` variable, if given, provides a path to the KAS config - /// file, which is read or written according to `KAS_CONFIG_MODE`. - /// If `KAS_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_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 - /// - `ReadWrite`: read on start-up, write on exit - /// - `WriteDefault`: generate platform-default configuration and write - /// it to the config path(s) specified, overwriting any existing config - /// - /// Note: in the future, the default will likely change to a read-write mode, - /// allowing changes to be written out. - pub fn from_env() -> Self { - let mut options = Options::default(); - - if let Ok(v) = var("KAS_CONFIG") { - 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() { - "READ" => ConfigMode::Read, - "READWRITE" => ConfigMode::ReadWrite, - "WRITEDEFAULT" => ConfigMode::WriteDefault, - other => { - log::error!("from_env: bad var KAS_CONFIG_MODE={other}"); - log::error!("from_env: supported config modes: READ, READWRITE, WRITEDEFAULT"); - options.config_mode - } - }; - } - - 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: T::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(), - } - - Ok(()) - } - - /// Load/save KAS config on start - /// - /// Requires feature "serde" to load/save config. - pub fn read_config(&self) -> Result { - #[cfg(feature = "serde")] - if !self.config_path.as_os_str().is_empty() { - return match self.config_mode { - #[cfg(feature = "serde")] - ConfigMode::Read | ConfigMode::ReadWrite => { - Ok(Format::guess_and_read_path(&self.config_path)?) - } - #[cfg(feature = "serde")] - ConfigMode::WriteDefault => { - let config: kas::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); - } - Ok(config) - } - }; - } - - Ok(Default::default()) - } - - /// Save all config (on exit or after changes) - /// - /// Requires feature "serde" to save config. - pub fn write_config>( - &self, - _config: &kas::event::Config, - _theme: &T, - ) -> 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/config.rs b/crates/kas-core/src/config/config.rs new file mode 100644 index 000000000..f3e0a3053 --- /dev/null +++ b/crates/kas-core/src/config/config.rs @@ -0,0 +1,263 @@ +// 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::{EventConfig, EventConfigMsg, EventWindowConfig}; +use super::{FontConfig, FontConfigMsg, ThemeConfig, ThemeConfigMsg}; +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; + +/// A message which may be used to update [`Config`] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ConfigMsg { + Event(EventConfigMsg), + Font(FontConfigMsg), + Theme(ThemeConfigMsg), +} + +/// Base configuration +/// +/// This is serializable (using `feature = "serde"`) with the following fields: +/// +/// > `event`: [`EventConfig`] \ +/// > `font`: [`FontConfig`] \ +/// > `shortcuts`: [`Shortcuts`] \ +/// > `theme`: [`ThemeConfig`] \ +/// > `max_fps`: `u32` +/// +/// For descriptions of configuration effects, see [`WindowConfig`] methods. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Config { + pub event: EventConfig, + + pub font: FontConfig, + + #[cfg_attr(feature = "serde", serde(default = "Shortcuts::platform_defaults"))] + pub shortcuts: Shortcuts, + + pub theme: ThemeConfig, + + /// 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, +} + +impl Default for Config { + fn default() -> Self { + Config { + event: EventConfig::default(), + font: Default::default(), + shortcuts: Shortcuts::platform_defaults(), + theme: Default::default(), + max_fps: defaults::max_fps(), + is_dirty: false, + } + } +} + +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 { + self.is_dirty + } +} + +/// Configuration, adapted for the application and window scale +#[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, + /// 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. + pub(crate) 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, + nav_focus: true, + frame_dur: Default::default(), + } + } + + /// Update window-specific/cached values + 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; + 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; + 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`] + pub fn base(&self) -> Ref { + self.config.borrow() + } + + /// 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() + } + } + + /// Access event config + pub fn event(&self) -> EventWindowConfig { + EventWindowConfig(self) + } + + /// Update event configuration + 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) + } 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; + c.font.set_size(pt_size) + } else { + Action::empty() + } + } + + /// Access shortcut config + pub fn shortcuts(&self) -> Ref { + 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 Action>(&self, f: F) -> Action { + if let Ok(mut c) = self.config.try_borrow_mut() { + c.is_dirty = true; + + f(&mut c.theme) + } else { + Action::empty() + } + } + + /// 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), + ConfigMsg::Theme(msg) => self.update_theme(|theme| theme.change_config(msg)), + } + } + + /// 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 max_fps() -> u32 { + 80 + } +} diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/config/event.rs similarity index 63% rename from crates/kas-core/src/event/config.rs rename to crates/kas-core/src/config/event.rs index fc2d19288..e6436cfe7 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/config/event.rs @@ -5,21 +5,19 @@ //! Event handling configuration -mod shortcuts; -pub use shortcuts::Shortcuts; - -use super::ModifiersState; 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, RefCell}; -use std::rc::Rc; +use std::cell::Ref; use std::time::Duration; -/// Configuration message used to update [`Config`] +/// A message which may be used to update [`EventConfig`] #[derive(Clone, Debug)] -pub enum ChangeConfig { +#[non_exhaustive] +pub enum EventConfigMsg { MenuDelay(u32), TouchSelectDelay(u32), ScrollFlickTimeout(u32), @@ -48,13 +46,12 @@ 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. +/// 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, @@ -88,21 +85,11 @@ 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 { +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(), @@ -114,95 +101,55 @@ 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) { +impl EventConfig { + pub(super) fn change_config(&mut self, msg: EventConfigMsg) -> Action { 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 = EventConfig::default(), } - self.is_dirty = true; + + Action::EVENT_CONFIG } } -/// 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, -} +pub struct EventWindowConfig<'a>(pub(super) &'a super::WindowConfig); -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()); - } - - /// Borrow access to the [`Config`] - pub fn borrow(&self) -> Ref { - self.config.borrow() +impl<'a> EventWindowConfig<'a> { + /// Access base (unscaled) event [`EventConfig`] + #[inline] + pub fn base(&self) -> Ref { + Ref::map(self.0.config.borrow(), |c| &c.event) } -} -impl WindowConfig { /// 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.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.config.borrow().touch_select_delay_ms.cast()) + Duration::from_millis(self.base().touch_select_delay_ms.cast()) } /// Controls activation of glide/momentum scrolling @@ -212,7 +159,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.base().scroll_flick_timeout_ms.cast()) } /// Scroll flick velocity decay: `(mul, sub)` @@ -227,15 +174,15 @@ 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.base().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) } @@ -248,45 +195,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.base().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.base().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.base().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 T, T>(&self, f: F) -> T { - let base = self.config.borrow(); - f(&base.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.base().touch_nav_focus } } @@ -368,8 +301,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/theme/config.rs b/crates/kas-core/src/config/font.rs similarity index 56% rename from crates/kas-core/src/theme/config.rs rename to crates/kas-core/src/config/font.rs index 2d3891e12..34f8faf7d 100644 --- a/crates/kas-core/src/theme/config.rs +++ b/crates/kas-core/src/config/font.rs @@ -3,40 +3,36 @@ // You may obtain a copy of the License in the LICENSE-APACHE file or at: // https://www.apache.org/licenses/LICENSE-2.0 -//! Theme configuration +//! Font configuration -use super::{ColorsSrgb, TextClass, ThemeConfig}; use crate::text::fonts::{self, AddMode, FontSelector}; +use crate::theme::TextClass; use crate::Action; use std::collections::BTreeMap; -use std::time::Duration; -/// 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 Config { - #[cfg_attr(feature = "serde", serde(skip))] - dirty: bool, - +pub struct FontConfig { /// 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, - - /// 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, + #[cfg_attr(feature = "serde", serde(default = "defaults::size"))] + pub size: f32, /// Font aliases, used when searching for a font family matching the key. /// /// Example: /// ```yaml - /// font_aliases: + /// aliases: /// sans-serif: /// mode: Prepend /// list: @@ -53,36 +49,23 @@ pub struct Config { /// /// Supported modes: `Prepend`, `Append`, `Replace`. #[cfg_attr(feature = "serde", serde(default))] - font_aliases: BTreeMap, + pub 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, - - /// Transition duration used in animations - #[cfg_attr(feature = "serde", serde(default = "defaults::transition_fade_ms"))] - transition_fade_ms: u32, + pub fonts: BTreeMap>, /// Text glyph rastering settings #[cfg_attr(feature = "serde", serde(default))] - raster: RasterConfig, + pub raster: RasterConfig, } -impl Default for Config { +impl Default for FontConfig { fn default() -> Self { - Config { - dirty: false, - font_size: defaults::font_size(), - active_scheme: Default::default(), - color_schemes: defaults::color_schemes(), - font_aliases: Default::default(), + FontConfig { + size: defaults::size(), + 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(), } } @@ -143,43 +126,15 @@ impl Default for RasterConfig { } /// Getters -impl Config { +impl FontConfig { /// 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 - } - - /// Active colour scheme (name) - /// - /// An empty string will resolve the default colour scheme. - #[inline] - pub fn active_scheme(&self) -> &str { - &self.active_scheme - } - - /// Iterate over all colour schemes - #[inline] - pub fn color_schemes_iter(&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() - } - - /// 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 size(&self) -> f32 { + self.size } /// Get an iterator over font mappings @@ -187,65 +142,32 @@ impl Config { pub fn iter_fonts(&self) -> impl Iterator)> { self.fonts.iter() } - - /// Get the cursor blink rate (delay) - #[inline] - pub fn cursor_blink_rate(&self) -> Duration { - Duration::from_millis(self.cursor_blink_rate_ms as u64) - } - - /// Get the fade duration used in transition animations - #[inline] - pub fn transition_fade_duration(&self) -> Duration { - Duration::from_millis(self.transition_fade_ms as u64) - } } /// Setters -impl Config { - /// Set font size - pub fn set_font_size(&mut self, pt_size: f32) { - self.dirty = true; - self.font_size = pt_size; - } - - /// 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 Config { - /// 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::RESIZE | Action::THEME_UPDATE - } else if self != other { - Action::REDRAW +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) -> Action { + if self.size != pt_size { + self.size = pt_size; + Action::THEME_UPDATE | Action::UPDATE } else { Action::empty() - }; - - *self = other.clone(); - action + } } } -impl ThemeConfig for Config { - #[cfg(feature = "serde")] - #[inline] - fn is_dirty(&self) -> bool { - self.dirty - } - +/// Other functions +impl FontConfig { /// Apply config effects which only happen on startup - fn apply_startup(&self) { - if !self.font_aliases.is_empty() { + pub(super) fn init(&self) { + if !self.aliases.is_empty() { fonts::library().update_db(|db| { - for (family, aliases) in self.font_aliases.iter() { + for (family, aliases) in self.aliases.iter() { db.add_aliases( family.to_string().into(), aliases.list.iter().map(|s| s.to_string().into()), @@ -258,7 +180,7 @@ impl ThemeConfig for Config { /// Get raster config #[inline] - fn raster(&self) -> &RasterConfig { + pub fn raster(&self) -> &RasterConfig { &self.raster } } @@ -280,18 +202,10 @@ mod defaults { AddMode::Prepend } - pub fn font_size() -> f32 { + pub fn size() -> f32 { 16.0 } - 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 - } - pub fn fonts() -> BTreeMap> { let mut selector = FontSelector::new(); selector.set_families(vec!["serif".into()]); @@ -302,14 +216,6 @@ mod defaults { list.iter().cloned().collect() } - pub fn cursor_blink_rate_ms() -> u32 { - 600 - } - - pub fn transition_fade_ms() -> u32 { - 150 - } - pub fn scale_steps() -> u8 { 4 } 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..a84c1773d --- /dev/null +++ b/crates/kas-core/src/config/mod.rs @@ -0,0 +1,27 @@ +// 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 config; +pub use config::{Config, ConfigMsg, WindowConfig}; + +mod event; +pub use event::{EventConfig, EventConfigMsg, EventWindowConfig, MousePan}; + +mod font; +pub use font::{FontConfig, FontConfigMsg, RasterConfig}; + +mod format; +pub use format::{Error, Format}; + +mod options; +pub use options::{ConfigMode, Options}; + +mod shortcuts; +pub use shortcuts::Shortcuts; + +mod theme; +pub use theme::{ThemeConfig, ThemeConfigMsg}; diff --git a/crates/kas-core/src/config/options.rs b/crates/kas-core/src/config/options.rs new file mode 100644 index 000000000..d220d9a0b --- /dev/null +++ b/crates/kas-core/src/config/options.rs @@ -0,0 +1,136 @@ +// 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 options + +#[cfg(feature = "serde")] use super::Format; +use super::{Config, Error}; +#[cfg(feature = "serde")] use crate::util::warn_about_error; +use std::env::var; +use std::path::PathBuf; + +/// Config mode +/// +/// See [`Options::from_env`] documentation. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum ConfigMode { + /// Read-only mode + Read, + /// Read-write mode + /// + /// This mode reads config on start and writes changes on exit. + ReadWrite, + /// Use default config and write out + /// + /// This mode only writes initial (default) config and does not update. + WriteDefault, +} + +/// Application configuration options +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Options { + /// Config file path. Default: empty. See `KAS_CONFIG` doc. + pub config_path: PathBuf, + /// Config mode. Default: Read. + pub config_mode: ConfigMode, +} + +impl Default for Options { + fn default() -> Self { + Options { + config_path: PathBuf::new(), + config_mode: ConfigMode::Read, + } + } +} + +impl Options { + /// Construct a new instance, reading from environment variables + /// + /// The following environment variables are read, in case-insensitive mode. + /// + /// # Config files + /// + /// WARNING: file formats are not stable and may not be compatible across + /// KAS versions (aside from patch versions)! + /// + /// The `KAS_CONFIG` variable, if given, provides a path to the KAS config + /// file, which is read or written according to `KAS_CONFIG_MODE`. + /// If `KAS_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 + /// - `ReadWrite`: read on start-up, write on exit + /// - `WriteDefault`: generate platform-default configuration and write + /// it to the config path(s) specified, overwriting any existing config + /// + /// Note: in the future, the default will likely change to a read-write mode, + /// allowing changes to be written out. + pub fn from_env() -> Self { + let mut options = Options::default(); + + if let Ok(v) = var("KAS_CONFIG") { + options.config_path = v.into(); + } + + if let Ok(mut v) = var("KAS_CONFIG_MODE") { + v.make_ascii_uppercase(); + options.config_mode = match v.as_str() { + "READ" => ConfigMode::Read, + "READWRITE" => ConfigMode::ReadWrite, + "WRITEDEFAULT" => ConfigMode::WriteDefault, + other => { + log::error!("from_env: bad var KAS_CONFIG_MODE={other}"); + log::error!("from_env: supported config modes: READ, READWRITE, WRITEDEFAULT"); + options.config_mode + } + }; + } + + options + } + + /// Load/save KAS config on start + /// + /// Requires feature "serde" to load/save config. + pub fn read_config(&self) -> Result { + #[cfg(feature = "serde")] + if !self.config_path.as_os_str().is_empty() { + return match self.config_mode { + #[cfg(feature = "serde")] + ConfigMode::Read | ConfigMode::ReadWrite => { + Ok(Format::guess_and_read_path(&self.config_path)?) + } + #[cfg(feature = "serde")] + ConfigMode::WriteDefault => { + 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); + } + Ok(config) + } + }; + } + + Ok(Default::default()) + } + + /// Save all config (on exit or after changes) + /// + /// Requires feature "serde" to save config. + 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)?; + } + } + + Ok(()) + } +} 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/config/theme.rs b/crates/kas-core/src/config/theme.rs new file mode 100644 index 000000000..b731fa2b8 --- /dev/null +++ b/crates/kas-core/src/config/theme.rs @@ -0,0 +1,200 @@ +// 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 + +//! 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 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. + 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 { + /// 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 + /// 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"))] + pub color_schemes: BTreeMap, + + /// Text cursor blink rate: delay between switching states + #[cfg_attr(feature = "serde", serde(default = "defaults::cursor_blink_rate_ms"))] + pub cursor_blink_rate_ms: u32, + + /// Transition duration used in animations + #[cfg_attr(feature = "serde", serde(default = "defaults::transition_fade_ms"))] + pub transition_fade_ms: u32, +} + +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(), + transition_fade_ms: defaults::transition_fade_ms(), + } + } +} + +impl ThemeConfig { + 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; + 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. + #[inline] + pub fn active_scheme(&self) -> &str { + &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) -> 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() + } + } + + /// Iterate over all colour schemes + #[inline] + 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<&ColorsSrgb> { + self.color_schemes.get(name) + } + + /// Get the active colour scheme + #[inline] + 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) -> Action { + self.color_schemes.insert(scheme.to_string(), colors); + Action::empty() + } + + /// Remove a colour scheme + 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) + #[inline] + pub fn cursor_blink_rate(&self) -> Duration { + Duration::from_millis(self.cursor_blink_rate_ms as u64) + } + + /// Get the fade duration used in transition animations + #[inline] + pub fn transition_fade_duration(&self) -> Duration { + Duration::from_millis(self.transition_fade_ms as u64) + } +} + +mod defaults { + use super::*; + + #[cfg(not(feature = "dark-light"))] + pub fn default_scheme() -> String { + "light".to_string() + } + + #[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(), + } + } + + 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 + } + + pub fn cursor_blink_rate_ms() -> u32 { + 600 + } + + pub fn transition_fade_ms() -> u32 { + 150 + } +} 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/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/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index ec8af102c..fda2bc720 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::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/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/config.rs b/crates/kas-core/src/event/cx/config.rs index 4d80415c9..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::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 21ca6ff40..9293cad37 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -11,10 +11,10 @@ use std::time::Duration; use super::*; use crate::cast::Conv; +use crate::config::ConfigMsg; use crate::draw::DrawShared; -use crate::event::config::ChangeConfig; 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 @@ -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,19 +158,13 @@ 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 #[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"), - } + pub fn change_config(&mut self, msg: ConfigMsg) { + self.action |= self.config.change_config(msg); } /// Set/unset a widget as disabled @@ -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 @@ -943,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/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index 803dc120c..bccaec4fc 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -16,14 +16,13 @@ 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::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; @@ -395,9 +394,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![]; @@ -441,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/event/cx/platform.rs b/crates/kas-core/src/event/cx/platform.rs index 308542dbe..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 @@ -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-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 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-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) 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/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/anim.rs b/crates/kas-core/src/theme/anim.rs index e4485b2da..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: &super::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/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/dimensions.rs b/crates/kas-core/src/theme/dimensions.rs index 849448363..d1636ba94 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::WindowConfig; use crate::dir::Directional; use crate::geom::{Rect, Size, Vec2}; use crate::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules, Stretch}; @@ -112,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(); @@ -159,19 +159,18 @@ pub struct Window { impl Window { pub fn new( dims: &Parameters, - config: &Config, - scale: f32, + config: &WindowConfig, 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(&config.theme()), } } - 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 8db9adf70..eedca0590 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -5,22 +5,24 @@ //! Flat theme +use std::cell::RefCell; use std::f32; 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::config::{Config, WindowConfig}; +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, InputState, Theme}; +use crate::theme::{ThemeDraw, ThemeSize}; +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. @@ -60,26 +62,6 @@ impl FlatTheme { let base = SimpleTheme::new(); 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. - #[inline] - #[must_use] - pub fn with_colours(mut self, scheme: &str) -> Self { - self.base = self.base.with_colours(scheme); - self - } } fn dimensions() -> dim::Parameters { @@ -105,30 +87,22 @@ 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 { - >::config(&self.base) - } - - fn apply_config(&mut self, config: &Self::Config) -> Action { - >::apply_config(&mut self.base, config) + fn init(&mut self, config: &RefCell) { + >::init(&mut self.base, config) } - fn init(&mut self, shared: &mut SharedState) { - >::init(&mut self.base, shared) - } - - fn new_window(&self, dpi_factor: f32) -> 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(), &self.base.config, dpi_factor, fonts) + dim::Window::new(&dimensions(), config, fonts) } - fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) { - w.update(&dimensions(), &self.base.config, dpi_factor); + fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) { + self.base.cols = config.theme().get_active_scheme().into(); + w.update(&dimensions(), config); } fn draw<'a>( @@ -161,32 +135,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() - } - - 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<'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 75e80f521..e40814572 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}; @@ -38,7 +36,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 traits::{Theme, ThemeConfig, ThemeControl, Window}; +pub use theme_dst::ThemeDst; +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 9ae76f332..f57438acb 100644 --- a/crates/kas-core/src/theme/multi.rs +++ b/crates/kas-core/src/theme/multi.rs @@ -5,13 +5,13 @@ //! Wrapper around mutliple themes, supporting run-time switching -use std::collections::HashMap; - -use super::{ColorsLinear, Config, Theme, ThemeDst, Window}; -use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; +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>; @@ -78,42 +78,38 @@ 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 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; + } + } + } - fn apply_config(&mut self, config: &Self::Config) -> Action { - let mut action = Action::empty(); for theme in &mut self.themes { - action |= theme.apply_config(config); + theme.init(config); } - action } - fn init(&mut self, shared: &mut SharedState) { - for theme in &mut self.themes { - theme.init(shared); + 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; + } } - } - fn new_window(&self, dpi_factor: f32) -> Self::Window { - self.themes[self.active].new_window(dpi_factor) + 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(&mut self, window: &mut Self::Window, config: &WindowConfig) { + self.themes[self.active].update_window(window, config); } fn draw<'a>( @@ -138,61 +134,3 @@ impl Theme for MultiTheme { self.themes[self.active].clear_color() } } - -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() - } - - 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 { - self.active = index; - return Action::RESIZE | Action::THEME_UPDATE; - } - } - Action::empty() - } -} diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index ab770449d..94fb4edf6 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -6,22 +6,26 @@ //! Simple theme use linear_map::LinearMap; +use std::cell::RefCell; use std::f32; 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::config::{Config, WindowConfig}; +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, InputState, Theme}; +use crate::theme::{SelectionStyle, ThemeDraw, ThemeSize}; +use crate::Id; + +use super::ColorsSrgb; /// A simple theme /// @@ -29,7 +33,6 @@ use kas::{Action, Id}; /// other themes. #[derive(Clone, Debug)] pub struct SimpleTheme { - pub config: Config, pub cols: ColorsLinear, dims: dim::Parameters, pub fonts: Option>>, @@ -45,34 +48,12 @@ impl SimpleTheme { /// Construct #[inline] pub fn new() -> Self { - let cols = ColorsLinear::default(); SimpleTheme { - config: Default::default(), - cols, + cols: ColorsSrgb::LIGHT.into(), // value is unimportant dims: Default::default(), fonts: None, } } - - /// 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. - #[inline] - #[must_use] - pub fn with_colours(mut self, name: &str) -> Self { - let _ = self.set_scheme(name); - self - } } pub struct DrawHandle<'a, DS: DrawSharedImpl> { @@ -86,44 +67,33 @@ 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 { - std::borrow::Cow::Borrowed(&self.config) - } - - fn apply_config(&mut self, config: &Self::Config) -> 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, _shared: &mut SharedState) { + 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( - self.config + config + .borrow() + .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(&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, &self.config, dpi_factor, fonts) + dim::Window::new(&self.dims, config, fonts) } - fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) { - w.update(&self.dims, &self.config, dpi_factor); + fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) { + self.cols = config.theme().get_active_scheme().into(); + w.update(&self.dims, config); } fn draw<'a>( @@ -156,40 +126,6 @@ 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 - } - - 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<'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 a9965e368..bb059436d 100644 --- a/crates/kas-core/src/theme/theme_dst.rs +++ b/crates/kas-core/src/theme/theme_dst.rs @@ -5,58 +5,34 @@ //! Stack-DST versions of theme traits -use std::any::Any; -use std::borrow::Cow; +use std::cell::RefCell; use super::{Theme, Window}; -use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; +use crate::config::{Config, WindowConfig}; +use crate::draw::{color, DrawIface, DrawSharedImpl}; 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(), - } - } -} +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 { - /// Get current configuration - fn config(&self) -> MaybeBoxed; - - /// Apply/set the passed config - fn apply_config(&mut self, config: &dyn Any) -> Action; - +pub trait ThemeDst { /// Theme initialisation /// /// See also [`Theme::init`]. - fn init(&mut self, shared: &mut SharedState); + fn init(&mut self, config: &RefCell); /// Construct per-window storage /// /// See also [`Theme::new_window`]. - fn new_window(&self, dpi_factor: f32) -> 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, dpi_factor: f32); + fn update_window(&mut self, window: &mut dyn Window, config: &WindowConfig); fn draw<'a>( &'a self, @@ -72,29 +48,18 @@ 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 apply_config(&mut self, config: &dyn Any) -> Action { - self.apply_config(config.downcast_ref().unwrap()) - } - - fn init(&mut self, shared: &mut SharedState) { - self.init(shared); + fn init(&mut self, config: &RefCell) { + self.init(config); } - fn new_window(&self, dpi_factor: f32) -> Box { - let window = >::new_window(self, dpi_factor); + 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, dpi_factor: f32) { + fn update_window(&mut 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 7d7f5e676..4eb618a96 100644 --- a/crates/kas-core/src/theme/traits.rs +++ b/crates/kas-core/src/theme/traits.rs @@ -5,92 +5,16 @@ //! Theme traits -use super::{ColorsLinear, ColorsSrgb, RasterConfig, ThemeDraw, ThemeSize}; -use crate::draw::{color, DrawIface, DrawSharedImpl, SharedState}; +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 a [`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 { - /// 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; - - /// 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 - /// it to switch themes. - fn set_theme(&mut self, _theme: &str) -> Action { - Action::empty() - } -} - -/// 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`. @@ -98,10 +22,7 @@ pub trait ThemeConfig: /// 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 { - /// The associated config type - type Config: ThemeConfig; - +pub trait Theme { /// The associated [`Window`] implementation. type Window: Window; @@ -111,12 +32,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: &Self::Config) -> Action; - /// Theme initialisation /// /// The toolkit must call this method before [`Theme::new_window`] @@ -124,10 +39,13 @@ 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: &RefCell); /// 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 @@ -137,12 +55,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(&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, dpi_factor: f32); + /// 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-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: { + 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-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 8da520135..2e0170d2b 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::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..ad8ada6e7 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::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 cf3886b56..fe3ceb8b3 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -5,22 +5,24 @@ //! Shaded theme +use std::cell::RefCell; use std::f32; use std::ops::Range; use std::time::Instant; use crate::{DrawShaded, DrawShadedImpl}; use kas::cast::traits::*; +use kas::config::{Config, WindowConfig}; 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, ThemeControl, ThemeDraw, ThemeSize}; -use kas::theme::{ColorsLinear, Config, FlatTheme, InputState, SimpleTheme, Theme}; +use kas::theme::{Background, 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)] @@ -40,24 +42,6 @@ impl ShadedTheme { let base = SimpleTheme::new(); 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. - #[must_use] - pub fn with_colours(mut self, scheme: &str) -> Self { - self.base = self.base.with_colours(scheme); - self - } } fn dimensions() -> dim::Parameters { @@ -92,30 +76,22 @@ 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 { - >::config(&self.base) + fn init(&mut self, config: &RefCell) { + >::init(&mut self.base, config) } - fn apply_config(&mut self, config: &Self::Config) -> Action { - >::apply_config(&mut self.base, config) - } - - fn init(&mut self, shared: &mut SharedState) { - >::init(&mut self.base, shared) - } - - fn new_window(&self, dpi_factor: f32) -> 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(), &self.base.config, dpi_factor, fonts) + dim::Window::new(&dimensions(), config, fonts) } - fn update_window(&self, w: &mut Self::Window, dpi_factor: f32) { - w.update(&dimensions(), &self.base.config, dpi_factor); + fn update_window(&mut self, w: &mut Self::Window, config: &WindowConfig) { + self.base.cols = config.theme().get_active_scheme().into(); + w.update(&dimensions(), config); } fn draw<'a>( @@ -148,32 +124,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() - } - - 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<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> where DS::Draw: DrawRoundedImpl + DrawShadedImpl, diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index 423bda02c..d07b6e262 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, @@ -815,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 e5333b4d9..7a708c6a8 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::{ConfigMsg, EventConfigMsg, MousePan}; use kas::prelude::*; impl_scope! { @@ -55,7 +55,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 +88,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 +105,46 @@ 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().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().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().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().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().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().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().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().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().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().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().touch_nav_focus, - ChangeConfig::TouchNavFocus, + |cx: &ConfigCx, _| cx.config().base().event.touch_nav_focus, + EventConfigMsg::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) diff --git a/examples/calculator.rs b/examples/calculator.rs index fd20790a9..f5d934370 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(())?; + let _ = 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..8fbefc54a 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(())?; + let _ = app.config_mut().font.set_size(24.0); + app.with(Window::new(counter(), "Counter")).run() } diff --git a/examples/gallery.rs b/examples/gallery.rs index 2a498cf32..e1ecdb3b3 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() { @@ -600,11 +611,17 @@ 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:?}"); - 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..457f50f44 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(())?; + let _ = app.config_mut().theme.set_active_scheme("dark"); + app.with(window).run() } diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 7617eafa5..d3fcb6a7b 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -59,11 +59,9 @@ 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(); + let mut app = kas::app::Default::with_theme(theme).build(())?; + 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 5aac3e222..3538e175d 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)?; + let _ = 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)