diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 324b13b90..349ecf82e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,13 +53,15 @@ jobs: - name: test (kas-macros) run: cargo test --manifest-path kas-macros/Cargo.toml --all-features - name: test (kas) - run: cargo test --all-features + run: | + cargo test + # Note: we must test serde without winit and with winit + cargo test --features serde + cargo test --all-features - name: test (kas-theme) run: | - # note: stack_dst + gat is broken - cargo test --manifest-path kas-theme/Cargo.toml --features stack_dst,unsize - cargo test --manifest-path kas-theme/Cargo.toml --no-default-features --features gat + cargo test --manifest-path kas-theme/Cargo.toml --all-features - name: test (kas-wgpu) run: | - # note: gat is broken - cargo test --manifest-path kas-wgpu/Cargo.toml --features nightly,shaping + cargo test + cargo test --manifest-path kas-wgpu/Cargo.toml --all-features diff --git a/Cargo.toml b/Cargo.toml index 585c195f7..7f5313fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ categories = ["gui"] repository = "https://github.com/kas-gui/kas" exclude = ["/screenshots"] +[package.metadata.docs.rs] +features = ["nightly", "stack_dst", "winit"] + [features] # Enables usage of unstable Rust features nightly = ["min_spec"] @@ -36,6 +39,17 @@ shaping = ["kas-text/shaping"] # Enable Markdown parsing markdown = ["kas-text/markdown"] +#TODO: once namespaced-features (cargo#5565) and weak-dep-features (cargo#8832) +# are stable, enable this and remove the serde feature requirement under dependencies.winit +# For now, this does work with nightly Cargo and -Z namespaced-features -Z weak-dep-features +# serde = ["dep:serde", "winit?/serde"] + +# Enable support for YAML (de)serialisation +yaml = ["serde", "serde_yaml"] + +# Enable support for JSON (de)serialisation +json = ["serde", "serde_json"] + [dependencies] log = "0.4" smallvec = "1.4" @@ -43,6 +57,10 @@ stack_dst = { version = "0.6", optional = true } bitflags = "1" # only used without winit unicode-segmentation = "1.7" linear-map = "1.2.0" +thiserror = "1.0.23" +serde = { version = "1.0.123", features = ["derive"], optional = true } +serde_json = { version = "1.0.61", optional = true } +serde_yaml = { version = "0.8.16", optional = true } [dependencies.kas-macros] version = "0.6.0" @@ -57,9 +75,7 @@ rev = "7c628156f9035abef2ffaba090c61de016b239cb" # Provides translations for several winit types version = "0.23" optional = true +features = ["serde"] [workspace] members = ["kas-macros", "kas-theme", "kas-wgpu"] - -[package.metadata.docs.rs] -features = ["nightly", "stack_dst", "winit"] diff --git a/README.md b/README.md index 238ecd118..f83554a47 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,9 @@ The `kas` crate has the following feature flags: usage but not for end users. (This only affects generated documentation.) - `winit`: adds compatibility code for winit's event and geometry types. This is currently the only functional windowing/event library. +- `serde`: adds (de)serialisation support to various types +- `json`: adds config (de)serialisation using JSON (implies `serde`) +- `yaml`: adds config (de)serialisation using YAML (implies `serde`) - `stack_dst`: some compatibility impls (see `kas-theme`'s documentation) diff --git a/kas-wgpu/Cargo.toml b/kas-wgpu/Cargo.toml index 96cf7acb9..7a8986ca0 100644 --- a/kas-wgpu/Cargo.toml +++ b/kas-wgpu/Cargo.toml @@ -11,6 +11,12 @@ repository = "https://github.com/kas-gui/kas" readme = "README.md" documentation = "https://docs.rs/kas-wgpu/" +[package.metadata.docs.rs] +# NOTE: clipboard feature is causing build failures +# https://github.com/kas-gui/kas/issues/83 +no-default-features = true +features = ["stack_dst"] + [features] default = ["clipboard", "stack_dst"] nightly = ["unsize", "kas/nightly", "kas-theme/nightly"] @@ -40,6 +46,7 @@ smallvec = "1.1" wgpu = "0.6.0" wgpu_glyph = "0.10.0" winit = "0.23.0" +thiserror = "1.0.23" [dependencies.clipboard] # Provides clipboard support @@ -49,13 +56,7 @@ optional = true [dev-dependencies] chrono = "0.4" env_logger = "0.7" -kas = { path = "..", features = ["markdown", "winit"] } +kas = { path = "..", features = ["markdown", "winit", "json", "yaml"] } [build-dependencies] glob = "0.3" - -[package.metadata.docs.rs] -# NOTE: clipboard feature is causing build failures -# https://github.com/kas-gui/kas/issues/83 -no-default-features = true -features = ["stack_dst"] diff --git a/kas-wgpu/examples/filter-list.rs b/kas-wgpu/examples/filter-list.rs index e392df7ea..e4992c439 100644 --- a/kas-wgpu/examples/filter-list.rs +++ b/kas-wgpu/examples/filter-list.rs @@ -44,6 +44,7 @@ mod data { #[cfg(feature = "generator")] mod data { use chrono::{DateTime, Duration, Local}; + use kas::conv::Conv; use kas::widget::view::{Accessor, FilterAccessor}; use std::{cell::RefCell, rc::Rc}; diff --git a/kas-wgpu/src/lib.rs b/kas-wgpu/src/lib.rs index 39a47a467..a07a15a37 100644 --- a/kas-wgpu/src/lib.rs +++ b/kas-wgpu/src/lib.rs @@ -23,7 +23,9 @@ pub mod options; mod shared; mod window; -use std::{error, fmt}; +use std::cell::RefCell; +use std::rc::Rc; +use thiserror::Error; use kas::event::UpdateHandle; use kas::WindowId; @@ -47,16 +49,21 @@ pub use wgpu_glyph as glyph; /// Some variants are undocumented. Users should not match these variants since /// they are not considered part of the public API. #[non_exhaustive] -#[derive(Debug)] +#[derive(Error, Debug)] pub enum Error { /// No suitable graphics adapter found /// /// This can be a driver/configuration issue or hardware limitation. Note /// that for now, `wgpu` only supports DX11, DX12, Vulkan and Metal. + #[error("no graphics adapter found")] NoAdapter, + /// Config load/save error + #[error("config load/save error")] + Config(#[from] kas::event::ConfigError), #[doc(hidden)] /// OS error during window creation - Window(OsError), + #[error("operating system error")] + Window(#[from] OsError), } impl From for Error { @@ -65,23 +72,6 @@ impl From for Error { } } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - match self { - Error::NoAdapter => write!(f, "no suitable graphics adapter found"), - Error::Window(e) => write!(f, "window creation error: {}", e), - } - } -} - -impl error::Error for Error {} - -impl From for Error { - fn from(ose: OsError) -> Self { - Error::Window(ose) - } -} - /// A toolkit over winit and WebGPU /// /// All KAS shells are expected to provide a similar `Toolkit` type and API. @@ -100,7 +90,7 @@ where /// Construct a new instance with default options. /// /// Environment variables may affect option selection; see documentation - /// of [`Options::from_env`]. + /// of [`Options::from_env`]. KAS config is provided by [`Options::config`]. #[inline] pub fn new(theme: T) -> Result { Self::new_custom((), theme, Options::from_env()) @@ -116,20 +106,42 @@ where /// The `custom` parameter accepts a custom draw pipe (see [`CustomPipeBuilder`]). /// Pass `()` if you don't have one. /// - /// The [`Options`] parameter allows direct specification of shell - /// options; usually, these are provided by [`Options::from_env`]. + /// The [`Options`] parameter allows direct specification of shell options; + /// usually, these are provided by [`Options::from_env`]. + /// KAS config is provided by [`Options::config`]. #[inline] pub fn new_custom>( custom: CB, theme: T, options: Options, + ) -> Result { + let el = EventLoop::with_user_event(); + let config = Rc::new(RefCell::new(options.config()?)); + let scale_factor = find_scale_factor(&el); + Ok(Toolkit { + el, + windows: vec![], + shared: SharedState::new(custom, theme, options, config, scale_factor)?, + }) + } + + /// Construct an instance with custom options and config + /// + /// This is like [`Toolkit::new_custom`], but allows KAS config to be + /// specified directly, instead of loading via [`Options::config`]. + #[inline] + pub fn new_custom_config>( + custom: CB, + theme: T, + options: Options, + config: Rc>, ) -> Result { let el = EventLoop::with_user_event(); let scale_factor = find_scale_factor(&el); Ok(Toolkit { el, windows: vec![], - shared: SharedState::new(custom, theme, options, scale_factor)?, + shared: SharedState::new(custom, theme, options, config, scale_factor)?, }) } diff --git a/kas-wgpu/src/options.rs b/kas-wgpu/src/options.rs index faee56a1d..d01d49889 100644 --- a/kas-wgpu/src/options.rs +++ b/kas-wgpu/src/options.rs @@ -5,13 +5,30 @@ //! Options +use super::Error; use log::warn; use std::env::var; +use std::path::PathBuf; pub use wgpu::{BackendBit, PowerPreference}; +/// Config mode +/// +/// See [`Options::from_env`] documentation. +#[derive(Clone, PartialEq, Hash)] +pub enum ConfigMode { + /// Read-only mode + Read, + /// Use default config and write out + WriteDefault, +} + /// Shell options #[derive(Clone, PartialEq, 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, /// Adapter power preference. Default value: low power. pub power_preference: PowerPreference, /// Adapter backend. Default value: PRIMARY (Vulkan/Metal/DX12). @@ -21,6 +38,8 @@ pub struct Options { impl Default for Options { fn default() -> Self { Options { + config_path: PathBuf::new(), + config_mode: ConfigMode::Read, power_preference: PowerPreference::LowPower, backends: BackendBit::PRIMARY, } @@ -32,6 +51,26 @@ impl Options { /// /// The following environment variables are read, in case-insensitive mode. /// + /// ### Config + /// + /// The `KAS_CONFIG` variable, if given, provides a path to the KAS config + /// file, where configuration can be read and/or written. + /// + /// WARNING: file formats are unstable! + /// + /// If `KAS_CONFIG` is not set, 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 + /// - `WriteDefault`: generate platform-default configuration, and write + /// it to the config path, overwriting any existing config + /// + /// Note: in the future, the default will likely change to a read-write mode, + /// allowing changes to be written out. + /// /// ### Power preference /// /// The `KAS_POWER_PREFERENCE` variable supports: @@ -54,6 +93,22 @@ impl Options { 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, + "WRITEDEFAULT" => ConfigMode::WriteDefault, + other => { + warn!("Unexpected environment value: KAS_CONFIG_MODE={}", other); + options.config_mode + } + }; + } + if let Ok(mut v) = var("KAS_POWER_PREFERENCE") { v.make_ascii_uppercase(); options.power_preference = match v.as_str() { @@ -100,4 +155,23 @@ impl Options { pub(crate) fn backend(&self) -> BackendBit { self.backends } + + /// Load KAS config + pub fn config(&self) -> Result { + if !self.config_path.as_os_str().is_empty() { + match self.config_mode { + ConfigMode::Read => Ok(kas::event::Config::from_path( + &self.config_path, + Default::default(), + )?), + ConfigMode::WriteDefault => { + let config: kas::event::Config = Default::default(); + config.write_path(&self.config_path, Default::default())?; + Ok(config) + } + } + } else { + Ok(Default::default()) + } + } } diff --git a/kas-wgpu/src/shared.rs b/kas-wgpu/src/shared.rs index 284283d5d..001b6541b 100644 --- a/kas-wgpu/src/shared.rs +++ b/kas-wgpu/src/shared.rs @@ -6,11 +6,12 @@ //! Shared state use log::{info, warn}; +use std::cell::RefCell; use std::num::NonZeroU32; +use std::rc::Rc; use crate::draw::{CustomPipe, CustomPipeBuilder, DrawPipe, DrawWindow, ShaderManager}; use crate::{Error, Options, WindowId}; -use kas::event::UpdateHandle; use kas_theme::Theme; #[cfg(feature = "clipboard")] @@ -26,6 +27,7 @@ pub struct SharedState { pub shaders: ShaderManager, pub draw: DrawPipe, pub theme: T, + pub config: Rc>, pub pending: Vec, /// Newly created windows need to know the scale_factor *before* they are /// created. This is used to estimate ideal window size. @@ -42,6 +44,7 @@ where custom: CB, mut theme: T, options: Options, + config: Rc>, scale_factor: f64, ) -> Result { #[cfg(feature = "clipboard")] @@ -84,6 +87,7 @@ where shaders, draw, theme, + config, pending: vec![], scale_factor, window_id: 0, @@ -148,5 +152,5 @@ pub enum PendingAction { CloseWindow(WindowId), ThemeResize, RedrawAll, - Update(UpdateHandle, u64), + Update(kas::event::UpdateHandle, u64), } diff --git a/kas-wgpu/src/window.rs b/kas-wgpu/src/window.rs index 4cbe88593..0524b13c2 100644 --- a/kas-wgpu/src/window.rs +++ b/kas-wgpu/src/window.rs @@ -99,7 +99,7 @@ where }; let swap_chain = shared.device.create_swap_chain(&surface, &sc_desc); - let mut mgr = ManagerState::new(); + let mut mgr = ManagerState::new(shared.config.clone()); let mut tkw = TkWindow::new(shared, &window, &mut theme_window); mgr.configure(&mut tkw, &mut *widget); diff --git a/src/conv.rs b/src/conv.rs index 3f13ce823..0eba24ca2 100644 --- a/src/conv.rs +++ b/src/conv.rs @@ -100,15 +100,9 @@ macro_rules! impl_via_as { impl_via_as!(i32: isize); impl_via_as!(isize: i64, i128); +impl_via_as!(u16: isize); impl_via_as!(u32: usize); -impl_via_as!(usize: u64, u128); - -impl Conv for isize { - #[inline] - fn conv(v: u16) -> isize { - isize::conv(i32::from(v)) - } -} +impl_via_as!(usize: i128, u64, u128); macro_rules! impl_via_as_neg_check { ($x:ty: $y:ty) => { @@ -131,7 +125,7 @@ impl_via_as_neg_check!(i16: u16, u32, u64, u128, usize); impl_via_as_neg_check!(i32: u32, u64, u128, usize); impl_via_as_neg_check!(i64: u64, u128); impl_via_as_neg_check!(i128: u128); -impl_via_as_neg_check!(isize: u32, u64, u128, usize); +impl_via_as_neg_check!(isize: u64, u128, usize); // Assumption: $y::MAX is representable as $x macro_rules! impl_via_as_max_check { @@ -150,10 +144,12 @@ macro_rules! impl_via_as_max_check { }; } +impl_via_as_max_check!(u8: i8); impl_via_as_max_check!(u16: i8, i16, u8); impl_via_as_max_check!(u32: i8, i16, i32, u8, u16); -impl_via_as_max_check!(u64: i8, i16, i32, i64, u8, u16, u32, usize); -impl_via_as_max_check!(u128: i8, i16, i32, i64, i128, u8, u16, u32, u64, usize); +impl_via_as_max_check!(u64: i8, i16, i32, i64, isize, u8, u16, u32, usize); +impl_via_as_max_check!(u128: i8, i16, i32, i64, i128, isize); +impl_via_as_max_check!(u128: u8, u16, u32, u64, usize); impl_via_as_max_check!(usize: i8, i16, i32, isize, u8, u16, u32); // Assumption: $y::MAX and $y::MIN are representable as $x @@ -176,8 +172,8 @@ macro_rules! impl_via_as_range_check { impl_via_as_range_check!(i16: i8, u8); impl_via_as_range_check!(i32: i8, i16, u8, u16); impl_via_as_range_check!(i64: i8, i16, i32, isize, u8, u16, u32); -impl_via_as_range_check!(i128: i8, i16, i32, i64, isize, u8, u16, u32, u64); -impl_via_as_range_check!(isize: i8, i16, i32); +impl_via_as_range_check!(i128: i8, i16, i32, i64, isize, u8, u16, u32, u64, usize); +impl_via_as_range_check!(isize: i8, i16, i32, u8, u16); macro_rules! impl_via_as_revert_check { ($x:ty: $y:ty) => { @@ -197,13 +193,13 @@ macro_rules! impl_via_as_revert_check { } impl_via_as_revert_check!(i32: f32); -impl_via_as_revert_check!(u32: f32); -impl_via_as_revert_check!(i64: f32, f64); +impl_via_as_revert_check!(u32: isize, f32); +impl_via_as_revert_check!(i64: usize, f32, f64); impl_via_as_revert_check!(u64: f32, f64); impl_via_as_revert_check!(i128: f32, f64); impl_via_as_revert_check!(u128: f32, f64); -impl_via_as_revert_check!(isize: f32, f64); -impl_via_as_revert_check!(usize: f32, f64); +impl_via_as_revert_check!(isize: u32, f32, f64); +impl_via_as_revert_check!(usize: i64, f32, f64); impl_via_as_revert_check!(f64: f32); /// Value conversion — from float diff --git a/src/dir.rs b/src/dir.rs index 052693442..be66e6a47 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -45,6 +45,7 @@ macro_rules! fixed { [($d:ident, $df:ident)] => { /// Fixed instantiation of [`Directional`] #[derive(Copy, Clone, Default, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct $d; impl Directional for $d { type Flipped = $df; @@ -66,6 +67,7 @@ fixed![(Right, Down), (Left, Up),]; /// /// This is a variable instantiation of [`Directional`]. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Direction { Right = 0, Down = 1, diff --git a/src/draw/colour.rs b/src/draw/colour.rs index 1a0ac0b97..ccd179cf9 100644 --- a/src/draw/colour.rs +++ b/src/draw/colour.rs @@ -10,6 +10,7 @@ /// NOTE: spelling standardisation is omitted for this type on the basis that /// is expected to be replaced in the near future. #[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Colour { pub r: f32, pub g: f32, diff --git a/src/event/config.rs b/src/event/config.rs new file mode 100644 index 000000000..7793ed7a1 --- /dev/null +++ b/src/event/config.rs @@ -0,0 +1,132 @@ +// 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 + +//! Event handling configuration + +use super::shortcuts::Shortcuts; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::path::Path; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConfigError { + #[cfg(feature = "yaml")] + #[error("config (de)serialisation to YAML failed")] + Yaml(#[from] serde_yaml::Error), + #[cfg(feature = "json")] + #[error("config (de)serialisation to JSON failed")] + Json(#[from] serde_json::Error), + #[error("error reading / writing config file")] + IoError(#[from] std::io::Error), + #[error("format not supported: {0}")] + UnsupportedFormat(ConfigFormat), +} + +/// Serialisation formats +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Error)] +pub enum ConfigFormat { + /// Not specified: guess from the path + #[error("no format")] + None, + /// JSON + #[error("JSON")] + Json, + /// TOML + #[error("TOML")] + Toml, + /// YAML + #[error("YAML")] + Yaml, + /// Error: unable to guess format + #[error("(unknown format)")] + Unknown, +} + +impl Default for ConfigFormat { + fn default() -> Self { + ConfigFormat::None + } +} + +/// Event handling configuration +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Config { + pub shortcuts: Shortcuts, +} + +impl Default for Config { + fn default() -> Self { + let mut shortcuts = Shortcuts::new(); + shortcuts.load_platform_defaults(); + Config { shortcuts } + } +} + +impl Config { + fn guess_format(path: &Path) -> ConfigFormat { + // use == since there is no OsStr literal + if let Some(ext) = path.extension() { + if ext == "json" { + ConfigFormat::Json + } else if ext == "toml" { + ConfigFormat::Toml + } else if ext == "yaml" { + ConfigFormat::Yaml + } else { + ConfigFormat::Unknown + } + } else { + ConfigFormat::Unknown + } + } + + /// Read from a path + pub fn from_path(path: &Path, mut format: ConfigFormat) -> Result { + if format == ConfigFormat::None { + format = Self::guess_format(path); + } + + match format { + #[cfg(feature = "json")] + ConfigFormat::Json => { + let r = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(serde_json::from_reader(r)?) + } + #[cfg(feature = "yaml")] + ConfigFormat::Yaml => { + let r = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(serde_yaml::from_reader(r)?) + } + _ => Err(ConfigError::UnsupportedFormat(format)), + } + } + + /// Write to a path + pub fn write_path(&self, path: &Path, mut format: ConfigFormat) -> Result<(), ConfigError> { + if format == ConfigFormat::None { + format = Self::guess_format(path); + } + + match format { + #[cfg(feature = "json")] + ConfigFormat::Json => { + let w = std::io::BufWriter::new(std::fs::File::create(path)?); + serde_json::to_writer_pretty(w, self)?; + Ok(()) + } + #[cfg(feature = "yaml")] + ConfigFormat::Yaml => { + let w = std::io::BufWriter::new(std::fs::File::create(path)?); + serde_yaml::to_writer(w, self)?; + Ok(()) + } + // NOTE: Toml is not supported since the `toml` crate does not support enums as map keys + _ => Err(ConfigError::UnsupportedFormat(format)), + } + } +} diff --git a/src/event/enums.rs b/src/event/enums.rs index 201b8a206..881ee4e7a 100644 --- a/src/event/enums.rs +++ b/src/event/enums.rs @@ -9,6 +9,9 @@ #![allow(unused)] +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + /// Describes the appearance of the mouse cursor. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/event/events.rs b/src/event/events.rs index 62437f3d0..364990d63 100644 --- a/src/event/events.rs +++ b/src/event/events.rs @@ -5,6 +5,9 @@ //! Event handling: events +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + #[allow(unused)] use super::{GrabMode, Manager, Response}; // for doc-links use super::{MouseButton, UpdateHandle, VirtualKeyCode}; @@ -173,6 +176,7 @@ pub enum Event { /// The exact mapping between the keyboard and these commands is OS-specific. /// In the future it should be customisable (see `shortcuts` module). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Command { /// Escape key /// diff --git a/src/event/manager.rs b/src/event/manager.rs index 6c4a4bc54..93b464dcc 100644 --- a/src/event/manager.rs +++ b/src/event/manager.rs @@ -11,7 +11,9 @@ use linear_map::{set::LinearSet, LinearMap}; use log::trace; use smallvec::SmallVec; +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use std::time::Instant; use std::u16; @@ -96,9 +98,9 @@ enum Pending { // `SmallVec` is used to keep contents in local memory. #[derive(Debug)] pub struct ManagerState { + config: Rc>, end_id: WidgetId, modifiers: ModifiersState, - shortcuts: shortcuts::Shortcuts, /// char focus is on same widget as sel_focus; otherwise its value is ignored char_focus: bool, sel_focus: Option, @@ -259,7 +261,9 @@ impl<'a> Manager<'a> { W: Widget + ?Sized, { use VirtualKeyCode as VK; - let opt_command = self.state.shortcuts.get(self.state.modifiers, vkey); + let config = self.state.config.borrow(); + let opt_command = config.shortcuts.get(self.state.modifiers, vkey); + drop(config); let shift = self.state.modifiers.shift(); if self.state.char_focus { diff --git a/src/event/manager/mgr_shell.rs b/src/event/manager/mgr_shell.rs index d6e1b6e00..ebbc0b98c 100644 --- a/src/event/manager/mgr_shell.rs +++ b/src/event/manager/mgr_shell.rs @@ -28,14 +28,11 @@ const FAKE_MOUSE_BUTTON: MouseButton = MouseButton::Other(0); impl ManagerState { /// Construct an event manager per-window data struct #[inline] - pub fn new() -> Self { - let mut shortcuts = shortcuts::Shortcuts::default(); - shortcuts.load_defaults(); - + pub fn new(config: Rc>) -> Self { ManagerState { + config, end_id: Default::default(), modifiers: ModifiersState::empty(), - shortcuts, char_focus: false, sel_focus: None, nav_focus: None, diff --git a/src/event/mod.rs b/src/event/mod.rs index deefd88b3..ca8b46f7c 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -67,6 +67,7 @@ //! //! [`WidgetId`]: crate::WidgetId +mod config; #[cfg(not(feature = "winit"))] mod enums; mod events; @@ -88,6 +89,7 @@ pub use winit::event::{ModifiersState, MouseButton, VirtualKeyCode}; #[cfg(feature = "winit")] pub use winit::window::CursorIcon; +pub use config::{Config, ConfigError}; #[cfg(not(feature = "winit"))] pub use enums::{CursorIcon, ModifiersState, MouseButton, VirtualKeyCode}; pub use events::*; @@ -125,6 +127,7 @@ fn size_of_virtual_key_codes() { /// custom message types are required to implement this via the /// [`derive(VoidMsg)`](../macros/index.html#the-derivevoidmsg-macro) macro. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum VoidMsg {} /// Alias for `Response` diff --git a/src/event/shortcuts.rs b/src/event/shortcuts.rs index b7b63e194..2a828ab6d 100644 --- a/src/event/shortcuts.rs +++ b/src/event/shortcuts.rs @@ -7,20 +7,30 @@ use super::{Command, ModifiersState, VirtualKeyCode}; use linear_map::LinearMap; +#[cfg(feature = "serde")] +use serde::de::{self, Deserialize, Deserializer, MapAccess, Unexpected, Visitor}; +#[cfg(feature = "serde")] +use serde::ser::{Serialize, SerializeMap, Serializer}; use std::collections::HashMap; +#[cfg(feature = "serde")] +use std::fmt; -#[derive(Default, Debug)] +/// Shortcut manager +#[derive(Debug)] pub struct Shortcuts { map: LinearMap>, } impl Shortcuts { - /// Load default shortcuts - /// - /// Note: text-editor move keys are repeated with shift so that e.g. - /// Shift+Home is matched. Such actions do not have unique names; the - /// consumer must check the status of the shift modifier directly. - pub fn load_defaults(&mut self) { + /// Construct, with no bindings + pub fn new() -> Self { + Shortcuts { + map: Default::default(), + } + } + + /// Load default shortcuts for the current platform + pub fn load_platform_defaults(&mut self) { use VirtualKeyCode as VK; #[cfg(target_os = "macos")] const CMD: ModifiersState = ModifiersState::LOGO; @@ -175,6 +185,10 @@ impl Shortcuts { } /// Match shortcuts + /// + /// Note: text-editor navigation keys (e.g. arrows, home/end) result in the + /// same output with and without Shift pressed. Editors should check the + /// status of the Shift modifier directly where this has an affect. pub fn get(&self, mut modifiers: ModifiersState, vkey: VirtualKeyCode) -> Option { if let Some(result) = self.map.get(&modifiers).and_then(|m| m.get(&vkey)) { return Some(*result); @@ -187,3 +201,159 @@ impl Shortcuts { None } } + +#[cfg(feature = "serde")] +fn state_to_string(state: ModifiersState) -> &'static str { + const SHIFT: ModifiersState = ModifiersState::SHIFT; + const CTRL: ModifiersState = ModifiersState::CTRL; + const ALT: ModifiersState = ModifiersState::ALT; + const SUPER: ModifiersState = ModifiersState::LOGO; + // we can't use match since OR patterns are unstable (rust#54883) + if state == ModifiersState::empty() { + "none" + } else if state == SUPER { + "super" + } else if state == ALT { + "alt" + } else if state == ALT | SUPER { + "alt-super" + } else if state == CTRL { + "ctrl" + } else if state == CTRL | SUPER { + "ctrl-super" + } else if state == CTRL | ALT { + "ctrl-alt" + } else if state == CTRL | ALT | SUPER { + "ctrl-alt-super" + } else if state == SHIFT { + "shift" + } else if state == SHIFT | SUPER { + "shift-super" + } else if state == SHIFT | ALT { + "alt-shift" + } else if state == SHIFT | ALT | SUPER { + "alt-shift-super" + } else if state == SHIFT | CTRL { + "ctrl-shift" + } else if state == SHIFT | CTRL | SUPER { + "ctrl-shift-super" + } else if state == SHIFT | CTRL | ALT { + "ctrl-alt-shift" + } else { + "ctrl-alt-shift-super" + } +} + +#[cfg(feature = "serde")] +impl Serialize for Shortcuts { + fn serialize(&self, s: S) -> Result + where + S: Serializer, + { + let mut map = s.serialize_map(Some(self.map.len()))?; + for (k, v) in &self.map { + map.serialize_entry(state_to_string(*k), v)?; + } + map.end() + } +} + +// #[derive(Error, Debug)] +// pub enum DeError { +// #[error("invalid modifier state: {0}")] +// State(String), +// } + +#[cfg(feature = "serde")] +struct ModifierStateVisitor(ModifiersState); +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for ModifierStateVisitor { + type Value = ModifierStateVisitor; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("none or ctrl or alt-shift-super etc.") + } + + fn visit_str(self, u: &str) -> Result { + let mut v = u; + let mut state = ModifiersState::empty(); + + if v.starts_with("ctrl") { + state |= ModifiersState::CTRL; + v = &v[v.len().min(4)..]; + } + if v.starts_with("-") { + v = &v[1..]; + } + if v.starts_with("alt") { + state |= ModifiersState::ALT; + v = &v[v.len().min(3)..]; + } + if v.starts_with("-") { + v = &v[1..]; + } + if v.starts_with("shift") { + state |= ModifiersState::SHIFT; + v = &v[v.len().min(5)..]; + } + if v.starts_with("-") { + v = &v[1..]; + } + if v.starts_with("super") { + state |= ModifiersState::LOGO; + v = &v[v.len().min(5)..]; + } + + if v.is_empty() || u == "none" { + Ok(ModifierStateVisitor(state)) + } else { + Err(E::invalid_value( + Unexpected::Str(u), + &"none or ctrl or alt-shift-super etc.", + )) + } + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for ModifierStateVisitor { + fn deserialize(d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_str(ModifierStateVisitor(Default::default())) + } +} + +#[cfg(feature = "serde")] +struct ShortcutsVisitor; +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for ShortcutsVisitor { + type Value = Shortcuts; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("{ : { : } }") + } + + fn visit_map(self, mut reader: A) -> Result + where + A: MapAccess<'de>, + { + let mut map = LinearMap::>::new(); + while let Some(key) = reader.next_key::()? { + let value = reader.next_value()?; + map.insert(key.0, value); + } + Ok(Shortcuts { map }) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Shortcuts { + fn deserialize(d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_map(ShortcutsVisitor) + } +} diff --git a/src/geom.rs b/src/geom.rs index d485e3156..33c7100bf 100644 --- a/src/geom.rs +++ b/src/geom.rs @@ -72,6 +72,7 @@ macro_rules! impl_common { /// A coordinate (or point) is an absolute position. One cannot add a point to /// a point. The difference between two points is an [`Offset`]. #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Coord(pub i32, pub i32); impl_common!(Coord); @@ -204,6 +205,7 @@ impl From for PhysicalPosition { /// /// This may be converted to [`Offset`] with `from` / `into`. #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Size(pub i32, pub i32); impl_common!(Size); @@ -371,6 +373,7 @@ impl From for winit::dpi::Size { /// /// This may be converted to [`Size`] with `from` / `into`. #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Offset(pub i32, pub i32); impl_common!(Offset); @@ -474,6 +477,7 @@ impl From for kas_text::Vec2 { /// The region is defined by a point `pos` and an extent `size`, allowing easy /// translations. It is empty unless `size` is positive on both axes. #[derive(Clone, Copy, Default, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Rect { pub pos: Coord, pub size: Size, diff --git a/src/geom/vector.rs b/src/geom/vector.rs index debfd2fa3..123445282 100644 --- a/src/geom/vector.rs +++ b/src/geom/vector.rs @@ -16,6 +16,7 @@ use std::ops::{Add, Div, Mul, Neg, Sub}; /// Typically it is expected that `a.le(b)`, although this is not required. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Quad { pub a: Vec2, pub b: Vec2, @@ -111,6 +112,7 @@ impl From for Quad { /// vectors (consider for `lhs = (0, 1), rhs = (1, 0)`). #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Vec2(pub f32, pub f32); /// 2D vector (double precision) @@ -124,6 +126,7 @@ pub struct Vec2(pub f32, pub f32); /// vectors (consider for `lhs = (0, 1), rhs = (1, 0)`). #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DVec2(pub f64, pub f64); macro_rules! impl_vec2 { @@ -388,6 +391,7 @@ impl_vec2!(DVec2, f64); /// Usually used for a 2D coordinate with a depth value. #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Vec3(pub f32, pub f32, pub f32); impl Vec3 {