diff --git a/Cargo.toml b/Cargo.toml index 4fb4d1ebe..988908b05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,10 @@ rustdoc-args = ["--cfg", "doc_cfg"] # markdown, resvg. Recommended also: clipboard, yaml (or some config format). minimal = ["wgpu", "winit", "wayland", "x11"] # All recommended features for optimal experience -default = ["minimal", "view", "yaml", "image", "resvg", "clipboard", "markdown", "shaping", "spawn"] +default = ["minimal", "view", "image", "resvg", "clipboard", "markdown", "shaping", "spawn"] # All standard test target features # NOTE: dynamic is excluded due to linker problems on Windows -stable = ["default", "serde", "json", "ron", "macros_log"] +stable = ["default", "serde", "toml", "yaml", "json", "ron", "macros_log"] # Enables "recommended" unstable features nightly = ["min_spec"] @@ -83,6 +83,9 @@ json = ["serde", "kas-core/json"] # Enable support for RON (de)serialisation ron = ["serde", "kas-core/ron"] +# Enable support for TOML (de)serialisation +toml = ["serde", "kas-core/toml"] + # Support image loading and decoding image = ["kas-widgets/image"] @@ -146,7 +149,3 @@ members = [ "crates/kas-view", "examples/mandlebrot", ] - -[patch.crates-io.winit] -git = "https://github.com/rust-windowing/winit.git" -rev = "cb58c49a90f17734e0405627130674d47c0b8f40" diff --git a/README.md b/README.md index e1f2e7156..2c466ae42 100644 --- a/README.md +++ b/README.md @@ -7,78 +7,50 @@ KAS GUI [![Docs](https://docs.rs/kas/badge.svg)](https://docs.rs/kas) ![Minimum rustc version](https://img.shields.io/badge/rustc-1.66+-lightgray.svg) -KAS is a pure-Rust GUI toolkit with stateful widgets: +KAS is a stateful, pure-Rust GUI toolkit supporting: -- [x] Pure, portable Rust -- [x] Very fast and CPU efficient -- [x] Flexible event handling without data races -- [x] Theme abstraction layer -- [x] [Winit] + [WGPU] shell supporting embedded accelerated content -- [ ] More portable shells: OpenGL, CPU-rendered, integration -- [x] [Complex text](https://github.com/kas-gui/kas-text/) -- [ ] OS integration: menus, fonts, IME -- [ ] Accessibility: screen reader, translation +- [x] Mostly declarative UI descriptions despite stateful widgets +- [x] Custom widgets using state for caches and input state (e.g. selection range) +- [x] Virtual scrolling (list or matrix), including support for external data sources +- [x] Theme abstraction including theme-driven animations and sizing +- [ ] Multiple renderer backends +- [ ] Integrated i18n support +- [ ] Accessibility tool integration +- [ ] Platform integration: persistent configuration, theme discovery, external menus, IME +- [x] Most of the basics you'd expect: complex text, fractional scaling, automatic margins +- [x] Extremely fast, monolithic binaries -![Animated](https://github.com/kas-gui/data-dump/blob/master/kas_0_11/video/animations.apng) -![Scalable](https://github.com/kas-gui/data-dump/blob/master/kas_0_10/image/scalable.png) - -[Winit]: https://github.com/rust-windowing/winit -[WGPU]: https://github.com/gfx-rs/wgpu - -### Documentation +### More - Wiki: [Getting started](https://github.com/kas-gui/kas/wiki/Getting-started), [Configuration](https://github.com/kas-gui/kas/wiki/Configuration), [Troubleshooting](https://github.com/kas-gui/kas/wiki/Troubleshooting) -- API docs: [kas](https://docs.rs/kas), [kas-core](https://docs.rs/kas-core), - [kas-widgets](https://docs.rs/kas-widgets), - [kas-wgpu](https://docs.rs/kas-wgpu) -- Prose: [Tutorials](https://kas-gui.github.io/tutorials/), - [Blog](https://kas-gui.github.io/blog/) - -### Examples - -See the [`examples`](examples) directory and -[kas-gui/7guis](https://github.com/kas-gui/7guis/). - - -Design ------- - -### Data or widget first? - -KAS attempts to blend several GUI models: - -- Like many older GUIs, there is a persistent tree of widgets with state -- Like Elm, event handling uses messages; unlike Elm, messages may be handled - anywhere in the widget tree (proceeding from leaf to root until handled) -- Widgets have a stable identity using a path over optionally explicit - components -- Like Model-View-Controller designs, data separation is possible; unlike Elm - this is not baked into the core of the design - -The results: - -- Natural support for multiple windows (there is no central data model) -- Widget trees (without MVC) are static and pre-allocated, though efficient - enough that maintaining (*many*) thousands - of not-currently-visible widgets isn't a problem -- Support for accessibility (only navigation aspects so far) -- MVC supports virtual scrolling (including persistent IDs for unrealised - widgets) -- MVC supports shared (`Rc` or `Arc`) data -- MVC and stateful widget designs feel like two different architectures - forced into the same UI toolkit +- [API docs](https://docs.rs/kas) +- Docs: [Tutorials](https://kas-gui.github.io/tutorials/), + [Blog](https://kas-gui.github.io/blog/), + [Design](https://github.com/kas-gui/design) +- Examples: [`examples` dir](examples), [kas-gui/7guis](https://github.com/kas-gui/7guis/). Crates and features ------------------- -`kas` is a meta-package over the core (`kas-core`), widget library -(`kas-widgets`), etc. [See here](https://kas-gui.github.io/tutorials/#kas). +[kas] is a meta-package serving as the library's public API, yet +containing no real code. Other crates in this repo: + +- [kas-core](https://docs.rs/kas-core): the core library +- [kas-widgets](https://docs.rs/kas-widgets): the main widget library +- [kas-view](https://docs.rs/kas-view): view widgets supporting virtual scrolling +- [kas-resvg](https://docs.rs/kas-resvg): extra widgets over [resvg](https://crates.io/crates/resvg) +- [kas-dylib](https://crates.io/crates/kas-dylib): helper crate to support dynamic linking +- kas-macros: proc-macro crate + +Significant external dependencies: -At this point in time, `kas-wgpu` is the only windowing/rendering implementation -thus `kas` uses this crate by default, though it is optional. +- [kas-text](https://crates.io/crates/kas-text): complex text support +- [impl-tools](https://crates.io/crates/impl-tools): `autoimpl` and `impl_scope` (extensible) macros +- [winit](https://github.com/rust-windowing/winit): platform window integration +- [wgpu](https://github.com/gfx-rs/wgpu): modern accelerated graphics API ### Feature flags @@ -86,10 +58,7 @@ The `kas` crate enables most important features by default, excepting those requiring nightly `rustc`. Other crates enable fewer features by default. See [Cargo.toml](https://github.com/kas-gui/kas/blob/master/Cargo.toml#L22). -[KAS-text]: https://github.com/kas-gui/kas-text/ -[winit]: https://github.com/rust-windowing/winit/ -[WGPU]: https://github.com/gfx-rs/wgpu -[`kas_wgpu::Options`]: https://docs.rs/kas-wgpu/latest/kas_wgpu/options/struct.Options.html +[kas]: https://docs.rs/kas Copyright and Licence diff --git a/crates/kas-core/Cargo.toml b/crates/kas-core/Cargo.toml index e187dd399..75f0de426 100644 --- a/crates/kas-core/Cargo.toml +++ b/crates/kas-core/Cargo.toml @@ -48,6 +48,9 @@ json = ["serde", "dep:serde_json"] # Enable support for RON (de)serialisation ron = ["serde", "dep:ron"] +# Enable support for TOML (de)serialisation +toml = ["serde", "dep:toml"] + # Enables clipboard read/write clipboard = ["dep:arboard", "dep:smithay-clipboard"] @@ -90,6 +93,7 @@ serde = { version = "1.0.123", features = ["derive"], optional = true } serde_json = { version = "1.0.61", optional = true } serde_yaml = { version = "0.9.9", optional = true } ron = { version = "0.8.0", package = "ron", optional = true } +toml = { version = "0.8.2", package = "toml", optional = true } num_enum = "0.7.0" dark-light = { version = "1.0", optional = true } raw-window-handle = "0.5.0" @@ -116,6 +120,7 @@ version = "0.5.0" # used in doc links [dependencies.winit] # Provides translations for several winit types -version = "0.29.1-beta" +version = "0.29.2" optional = true default-features = false +features = ["rwh_05"] diff --git a/crates/kas-core/src/config.rs b/crates/kas-core/src/config.rs index e6573cf40..c53ca8af2 100644 --- a/crates/kas-core/src/config.rs +++ b/crates/kas-core/src/config.rs @@ -7,6 +7,7 @@ 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; @@ -17,7 +18,7 @@ use thiserror::Error; /// Config mode /// /// See [`Options::from_env`] documentation. -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum ConfigMode { /// Read-only mode Read, @@ -54,6 +55,16 @@ pub enum Error { #[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), @@ -138,6 +149,11 @@ impl Format { 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)) @@ -149,27 +165,34 @@ impl Format { #[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 w = std::io::BufWriter::new(std::fs::File::create(path)?); - serde_json::to_writer_pretty(w, value)?; + let text = serde_json::to_string_pretty(value)?; + std::fs::write(path, &text)?; Ok(()) } #[cfg(feature = "yaml")] Format::Yaml => { - let w = std::io::BufWriter::new(std::fs::File::create(path)?); - serde_yaml::to_writer(w, value)?; + let text = serde_yaml::to_string(value)?; + std::fs::write(path, text)?; Ok(()) } #[cfg(feature = "ron")] Format::Ron => { - let w = std::io::BufWriter::new(std::fs::File::create(path)?); let pretty = ron::ser::PrettyConfig::default(); - ron::ser::to_writer_pretty(w, value, pretty)?; + 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(()) } - // NOTE: Toml is not supported since the `toml` crate does not support enums as map keys _ => { let _ = (path, value); // squelch unused warnings Err(Error::UnsupportedFormat(self)) @@ -195,7 +218,7 @@ impl Format { } /// Shell options -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Options { /// Config file path. Default: empty. See `KAS_CONFIG` doc. pub config_path: PathBuf, @@ -284,8 +307,7 @@ impl Options { match self.config_mode { #[cfg(feature = "serde")] ConfigMode::Read | ConfigMode::ReadWrite if self.theme_config_path.is_file() => { - let config: T::Config = - kas::config::Format::guess_and_read_path(&self.theme_config_path)?; + 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); @@ -294,10 +316,11 @@ impl Options { ConfigMode::WriteDefault if !self.theme_config_path.as_os_str().is_empty() => { let config = theme.config(); config.apply_startup(); - kas::config::Format::guess_and_write_path( - &self.theme_config_path, - config.as_ref(), - )?; + 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(), } @@ -314,12 +337,14 @@ impl Options { return match self.config_mode { #[cfg(feature = "serde")] ConfigMode::Read | ConfigMode::ReadWrite => { - Ok(kas::config::Format::guess_and_read_path(&self.config_path)?) + Ok(Format::guess_and_read_path(&self.config_path)?) } #[cfg(feature = "serde")] ConfigMode::WriteDefault => { let config: kas::event::Config = Default::default(); - kas::config::Format::guess_and_write_path(&self.config_path, &config)?; + if let Err(error) = Format::guess_and_write_path(&self.config_path, &config) { + warn_about_error("failed to write default config: ", &error); + } Ok(config) } }; @@ -339,14 +364,11 @@ impl Options { #[cfg(feature = "serde")] if self.config_mode == ConfigMode::ReadWrite { if !self.config_path.as_os_str().is_empty() && _config.is_dirty() { - kas::config::Format::guess_and_write_path(&self.config_path, &_config)?; + 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() { - kas::config::Format::guess_and_write_path( - &self.theme_config_path, - theme_config.as_ref(), - )?; + Format::guess_and_write_path(&self.theme_config_path, theme_config.as_ref())?; } } diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs index eff6a8c07..3220da99d 100644 --- a/crates/kas-core/src/core/widget.rs +++ b/crates/kas-core/src/core/widget.rs @@ -6,6 +6,7 @@ //! Widget and Events traits use super::{Layout, Node}; +#[allow(unused)] use crate::event::Used; use crate::event::{ConfigCx, Event, EventCx, IsUsed, Scroll, Unused}; use crate::{Erased, Id}; use kas_macros::autoimpl; @@ -173,8 +174,6 @@ pub trait Events: Widget + Sized { /// /// Default implementation of `handle_event`: do nothing; return /// [`Unused`]. - /// - /// Use [`EventCx::send`] instead of calling this method. fn handle_event(&mut self, cx: &mut EventCx, data: &Self::Data, event: Event) -> IsUsed { let _ = (cx, data, event); Unused @@ -184,10 +183,10 @@ pub trait Events: Widget + Sized { /// /// This is an optional event handler (see [documentation](crate::event)). /// - /// May cause a panic if this method returns [`Unused`] but does - /// affect `cx` (e.g. by calling [`EventCx::set_scroll`] or leaving a - /// message on the stack, possibly from [`EventCx::send`]). - /// This is considered a corner-case and not currently supported. + /// The method should *either* return [`Used`] or return [`Unused`] without + /// modifying `cx`; attempting to do otherwise (e.g. by calling + /// [`EventCx::set_scroll`] or leaving a message on the stack when returning + /// [`Unused`]) will result in a panic. /// /// Default implementation: return [`Unused`]. fn steal_event( diff --git a/crates/kas-core/src/event/config/shortcuts.rs b/crates/kas-core/src/event/config/shortcuts.rs index 45c5f8f0f..cdd1f4b06 100644 --- a/crates/kas-core/src/event/config/shortcuts.rs +++ b/crates/kas-core/src/event/config/shortcuts.rs @@ -7,17 +7,14 @@ use crate::event::{Command, Key, ModifiersState}; 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; +use winit::keyboard::NamedKey; /// Shortcut manager #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, Debug, PartialEq)] pub struct Shortcuts { + // NOTE: we do not permit Key::Dead(None) here map: LinearMap>, } @@ -51,14 +48,14 @@ impl Shortcuts { let modifiers = ModifiersState::empty(); let map = self.map.entry(modifiers).or_insert_with(Default::default); let shortcuts = [ - (Key::F1, Command::Help), - (Key::F2, Command::Rename), - (Key::F3, Command::FindNext), - (Key::F5, Command::Refresh), - (Key::F7, Command::SpellCheck), - (Key::F8, Command::Debug), - (Key::F10, Command::Menu), - (Key::F11, Command::Fullscreen), + (NamedKey::F1.into(), Command::Help), + (NamedKey::F2.into(), Command::Rename), + (NamedKey::F3.into(), Command::FindNext), + (NamedKey::F5.into(), Command::Refresh), + (NamedKey::F7.into(), Command::SpellCheck), + (NamedKey::F8.into(), Command::Debug), + (NamedKey::F10.into(), Command::Menu), + (NamedKey::F11.into(), Command::Fullscreen), ]; map.extend(shortcuts.iter().cloned()); } @@ -68,7 +65,7 @@ impl Shortcuts { { let modifiers = ModifiersState::SHIFT; let map = self.map.entry(modifiers).or_insert_with(Default::default); - map.insert(Key::F3, Command::FindPrevious); + map.insert(NamedKey::F3.into(), Command::FindPrevious); } // Alt (Option on MacOS) @@ -77,11 +74,11 @@ impl Shortcuts { #[cfg(not(target_os = "macos"))] { let shortcuts = [ - (Key::F4, Command::Close), - (Key::ArrowLeft, Command::NavPrevious), - (Key::ArrowRight, Command::NavNext), - (Key::ArrowUp, Command::NavParent), - (Key::ArrowDown, Command::NavDown), + (NamedKey::F4.into(), Command::Close), + (NamedKey::ArrowLeft.into(), Command::NavPrevious), + (NamedKey::ArrowRight.into(), Command::NavNext), + (NamedKey::ArrowUp.into(), Command::NavParent), + (NamedKey::ArrowDown.into(), Command::NavDown), ]; map.extend(shortcuts.iter().cloned()); } @@ -89,11 +86,11 @@ impl Shortcuts { { // Missing functionality: move to start/end of paragraph on (Shift)+Alt+Up/Down let shortcuts = [ - (Key::ArrowLeft, Command::WordLeft), - (Key::ArrowRight, Command::WordRight), + (NamedKey::ArrowLeft.into(), Command::WordLeft), + (NamedKey::ArrowRight.into(), Command::WordRight), ]; - map.insert(Key::Delete, Command::DelWordBack); + map.insert(NamedKey::Delete.into(), Command::DelWordBack); map.extend(shortcuts.iter().cloned()); // Shift + Option @@ -118,21 +115,20 @@ impl Shortcuts { (Key::Character("t".into()), Command::TabNew), (Key::Character("u".into()), Command::Underline), (Key::Character("v".into()), Command::Paste), - (Key::Character("]".into()), Command::Paste), (Key::Character("w".into()), Command::Close), (Key::Character("x".into()), Command::Cut), (Key::Character("z".into()), Command::Undo), - (Key::Tab, Command::TabNext), + (NamedKey::Tab.into(), Command::TabNext), ]; map.extend(shortcuts.iter().cloned()); #[cfg(target_os = "macos")] { let shortcuts = [ (Key::Character("g".into()), Command::FindNext), - (Key::ArrowUp, Command::DocHome), - (Key::ArrowDown, Command::DocEnd), - (Key::ArrowLeft, Command::Home), - (Key::ArrowRight, Command::End), + (NamedKey::ArrowUp.into(), Command::DocHome), + (NamedKey::ArrowDown.into(), Command::DocEnd), + (NamedKey::ArrowLeft.into(), Command::Home), + (NamedKey::ArrowRight.into(), Command::End), ]; map.extend(shortcuts.iter().cloned()); } @@ -145,16 +141,16 @@ impl Shortcuts { map.extend(shortcuts.iter().cloned()); let shortcuts = [ - (Key::ArrowUp, Command::ViewUp), - (Key::ArrowDown, Command::ViewDown), - (Key::ArrowLeft, Command::WordLeft), - (Key::ArrowRight, Command::WordRight), - (Key::Backspace, Command::DelWordBack), - (Key::Delete, Command::DelWord), - (Key::Home, Command::DocHome), - (Key::End, Command::DocEnd), - (Key::PageUp, Command::TabPrevious), - (Key::PageDown, Command::TabNext), + (NamedKey::ArrowUp.into(), Command::ViewUp), + (NamedKey::ArrowDown.into(), Command::ViewDown), + (NamedKey::ArrowLeft.into(), Command::WordLeft), + (NamedKey::ArrowRight.into(), Command::WordRight), + (NamedKey::Backspace.into(), Command::DelWordBack), + (NamedKey::Delete.into(), Command::DelWord), + (NamedKey::Home.into(), Command::DocHome), + (NamedKey::End.into(), Command::DocEnd), + (NamedKey::PageUp.into(), Command::TabPrevious), + (NamedKey::PageDown.into(), Command::TabNext), ]; map.extend(shortcuts.iter().cloned()); @@ -178,7 +174,7 @@ impl Shortcuts { let shortcuts = [ (Key::Character("a".into()), Command::Deselect), (Key::Character("z".into()), Command::Redo), - (Key::Tab, Command::TabPrevious), + (NamedKey::Tab.into(), Command::TabPrevious), ]; map.extend(shortcuts.iter().cloned()); #[cfg(target_os = "macos")] @@ -186,10 +182,10 @@ impl Shortcuts { let shortcuts = [ (Key::Character("g".into()), Command::FindPrevious), (Key::Character(":".into()), Command::SpellCheck), - (Key::ArrowUp, Command::DocHome), - (Key::ArrowDown, Command::DocEnd), - (Key::ArrowLeft, Command::Home), - (Key::ArrowRight, Command::End), + (NamedKey::ArrowUp.into(), Command::DocHome), + (NamedKey::ArrowDown.into(), Command::DocEnd), + (NamedKey::ArrowLeft.into(), Command::Home), + (NamedKey::ArrowRight.into(), Command::End), ]; map.extend(shortcuts.iter().cloned()); } @@ -222,164 +218,367 @@ impl Shortcuts { } #[cfg(feature = "serde")] -fn state_to_string(state: ModifiersState) -> &'static str { - const SHIFT: ModifiersState = ModifiersState::SHIFT; - const CONTROL: ModifiersState = ModifiersState::CONTROL; - const ALT: ModifiersState = ModifiersState::ALT; - const SUPER: ModifiersState = ModifiersState::SUPER; - // 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 == CONTROL { - "ctrl" - } else if state == CONTROL | SUPER { - "ctrl-super" - } else if state == CONTROL | ALT { - "ctrl-alt" - } else if state == CONTROL | 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 | CONTROL { - "ctrl-shift" - } else if state == SHIFT | CONTROL | SUPER { - "ctrl-shift-super" - } else if state == SHIFT | CONTROL | ALT { - "ctrl-alt-shift" - } else { - "ctrl-alt-shift-super" +mod common { + use super::{Command, Key, ModifiersState, NamedKey}; + use serde::de::{self, Deserializer, Visitor}; + use serde::ser::Serializer; + use serde::{Deserialize, Serialize}; + use std::fmt; + use winit::keyboard::{NativeKey, SmolStr}; + + /// A subset of [`Key`] which serialises to a simple value usable as a map key + #[derive(Deserialize)] + #[serde(untagged)] + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] + pub(super) enum SimpleKey { + Named(NamedKey), + Char(char), + } + + impl From for Key { + fn from(sk: SimpleKey) -> Self { + match sk { + SimpleKey::Named(key) => Key::Named(key), + SimpleKey::Char(c) => { + let mut buf = [0; 4]; + let s = c.encode_utf8(&mut buf); + Key::Character(SmolStr::new(s)) + } + } + } + } + + // NOTE: the only reason we don't use derive is that TOML does not support char as a map key, + // thus we must convrt with char::encode_utf8. See toml-lang/toml#1001 + impl Serialize for SimpleKey { + fn serialize(&self, s: S) -> Result { + match self { + SimpleKey::Named(key) => key.serialize(s), + SimpleKey::Char(c) => { + let mut buf = [0; 4]; + let cs = c.encode_utf8(&mut buf); + s.serialize_str(cs) + } + } + } + } + + /// A subset of [`Key`], excluding anything which is a [`SimpleKey`] + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] + pub(super) enum ComplexKey { + Character(Str), + Dead(char), + #[serde(untagged)] + Unidentified(NativeKey), + } + + impl From> for Key { + fn from(ck: ComplexKey) -> Self { + match ck { + ComplexKey::Character(c) => Key::Character(c), + ComplexKey::Dead(c) => Key::Dead(Some(c)), + ComplexKey::Unidentified(code) => Key::Unidentified(code), + } + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] + pub(super) struct ModifiersStateDeser(pub ModifiersState); + + impl Serialize for ModifiersStateDeser { + fn serialize(&self, serializer: S) -> Result { + const SHIFT: ModifiersState = ModifiersState::SHIFT; + const CONTROL: ModifiersState = ModifiersState::CONTROL; + const ALT: ModifiersState = ModifiersState::ALT; + const SUPER: ModifiersState = ModifiersState::SUPER; + + let s = match self.0 { + state if state == ModifiersState::empty() => "none", + SUPER => "super", + ALT => "alt", + state if state == ALT | SUPER => "alt-super", + state if state == CONTROL => "ctrl", + state if state == CONTROL | SUPER => "ctrl-super", + state if state == CONTROL | ALT => "ctrl-alt", + state if state == CONTROL | ALT | SUPER => "ctrl-alt-super", + SHIFT => "shift", + state if state == SHIFT | SUPER => "shift-super", + state if state == SHIFT | ALT => "alt-shift", + state if state == SHIFT | ALT | SUPER => "alt-shift-super", + state if state == SHIFT | CONTROL => "ctrl-shift", + state if state == SHIFT | CONTROL | SUPER => "ctrl-shift-super", + state if state == SHIFT | CONTROL | ALT => "ctrl-alt-shift", + _ => "ctrl-alt-shift-super", + }; + + serializer.serialize_str(s) + } + } + + impl<'de> Visitor<'de> for ModifiersStateDeser { + type Value = ModifiersStateDeser; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("none or (sub-set of) ctrl-alt-shift-super") + } + + fn visit_str(self, u: &str) -> Result { + let mut v = u; + let mut state = ModifiersState::empty(); + + let adv_dash_if_not_empty = |v: &mut &str| { + if !v.is_empty() { + if v.starts_with('-') { + *v = &v[1..]; + } + } + }; + + if v.starts_with("ctrl") { + state |= ModifiersState::CONTROL; + v = &v[v.len().min(4)..]; + adv_dash_if_not_empty(&mut v); + } + if v.starts_with("alt") { + state |= ModifiersState::ALT; + v = &v[v.len().min(3)..]; + adv_dash_if_not_empty(&mut v); + } + if v.starts_with("shift") { + state |= ModifiersState::SHIFT; + v = &v[v.len().min(5)..]; + adv_dash_if_not_empty(&mut v); + } + if v.starts_with("super") { + state |= ModifiersState::SUPER; + v = &v[v.len().min(5)..]; + } + + if v.is_empty() || u == "none" { + Ok(ModifiersStateDeser(state)) + } else { + Err(E::invalid_value( + de::Unexpected::Str(u), + &"none or (sub-set of) ctrl-alt-shift-super", + )) + } + } + } + + impl<'de> Deserialize<'de> for ModifiersStateDeser { + fn deserialize>(d: D) -> Result { + d.deserialize_str(ModifiersStateDeser(Default::default())) + } + } + + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] + pub(super) struct MiscRule { + #[serde(rename = "modifiers")] + pub(super) mods: ModifiersStateDeser, + #[serde(flatten)] + pub(super) key: ComplexKey, + #[serde(rename = "command")] + pub(super) cmd: Command, } } #[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 (state, bindings) in &self.map { - map.serialize_key(state_to_string(*state))?; - - // Sort items in the hash-map to ensure stable order - // NOTE: We need a "map type" to ensure entries are serialised as - // a map, not as a list. BTreeMap is easier than a shim over a Vec. - // TODO: winit::keyboard::Key does not support Ord! - // let bindings: std::collections::BTreeMap<_, _> = bindings.iter().collect(); - map.serialize_value(&bindings)?; - } - map.end() +mod ser { + use super::common::{ComplexKey, MiscRule, ModifiersStateDeser, SimpleKey}; + use super::{Key, Shortcuts}; + use serde::ser::{Serialize, SerializeMap, Serializer}; + + fn unpack_key<'a>(key: Key<&'a str>) -> Result> { + match key { + Key::Named(named) => Ok(SimpleKey::Named(named)), + Key::Character(c) => { + let mut iter = c.chars(); + if let Some(c) = iter.next() { + if iter.next().is_none() { + return Ok(SimpleKey::Char(c)); + } + } + Err(ComplexKey::Character(c)) + } + Key::Unidentified(code) => Err(ComplexKey::Unidentified(code)), + Key::Dead(None) => panic!("invalid shortcut"), + Key::Dead(Some(c)) => Err(ComplexKey::Dead(c)), + } } -} -// #[derive(Error, Debug)] -// pub enum DeError { -// #[error("invalid modifier state: {0}")] -// State(String), -// } + impl Serialize for Shortcuts { + fn serialize(&self, s: S) -> Result { + // Use BTreeMap for stable order of output + use std::collections::BTreeMap; + + let mut serializer = s.serialize_map(Some(self.map.len() + 1))?; + let mut misc = Vec::new(); + + for (state, key_cmds) in self.map.iter() { + let mods = ModifiersStateDeser(*state); + let mut map = BTreeMap::new(); + + for (key, cmd) in key_cmds.iter() { + match unpack_key(key.as_ref()) { + Ok(sk) => { + map.insert(sk, *cmd); + } + Err(key) => { + let cmd = *cmd; + misc.push(MiscRule { mods, key, cmd }); + } + } + } + + // Keys are now sorted and filtered + if !map.is_empty() { + serializer.serialize_key(&mods)?; + serializer.serialize_value(&map)?; + } + } + + if !misc.is_empty() { + serializer.serialize_key("other")?; + misc.sort(); + serializer.serialize_value(&misc)?; + } + serializer.end() + } + } +} #[cfg(feature = "serde")] -struct ModifierStateVisitor(ModifiersState); -#[cfg(feature = "serde")] -impl<'de> Visitor<'de> for ModifierStateVisitor { - type Value = ModifierStateVisitor; +mod deser { + use super::common::{MiscRule, ModifiersStateDeser, SimpleKey}; + use super::{Command, Key, ModifiersState, Shortcuts}; + use linear_map::LinearMap; + use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, Visitor}; + use std::collections::HashMap; + use std::fmt; + + enum OptModifiersStateDeser { + State(ModifiersStateDeser), + Other, + } - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("none or ctrl or alt-shift-super etc.") + impl<'de> Deserialize<'de> for OptModifiersStateDeser { + fn deserialize>(d: D) -> Result { + d.deserialize_str(OptModifiersStateDeser::Other) + } } - fn visit_str(self, u: &str) -> Result { - let mut v = u; - let mut state = ModifiersState::empty(); + impl<'de> Visitor<'de> for OptModifiersStateDeser { + type Value = OptModifiersStateDeser; - if v.starts_with("ctrl") { - state |= ModifiersState::CONTROL; - v = &v[v.len().min(4)..]; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("none or (sub-set of) ctrl-alt-shift-super or other") } - if v.starts_with('-') { - v = &v[1..]; + + fn visit_str(self, u: &str) -> Result { + if u == "other" { + Ok(OptModifiersStateDeser::Other) + } else { + ModifiersStateDeser::visit_str(ModifiersStateDeser(Default::default()), u) + .map(OptModifiersStateDeser::State) + } } - if v.starts_with("alt") { - state |= ModifiersState::ALT; - v = &v[v.len().min(3)..]; + } + + struct DeserSimple<'a>(&'a mut HashMap); + + impl<'a, 'de> DeserializeSeed<'de> for DeserSimple<'a> { + type Value = (); + + fn deserialize>(self, d: D) -> Result { + d.deserialize_map(self) } - if v.starts_with('-') { - v = &v[1..]; + } + + impl<'a, 'de> Visitor<'de> for DeserSimple<'a> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") } - if v.starts_with("shift") { - state |= ModifiersState::SHIFT; - v = &v[v.len().min(5)..]; + + fn visit_map(self, mut reader: A) -> Result + where + A: de::MapAccess<'de>, + { + while let Some(sk) = reader.next_key::()? { + let key: Key = sk.into(); + let cmd: Command = reader.next_value()?; + self.0.insert(key, cmd); + } + + Ok(()) } - if v.starts_with('-') { - v = &v[1..]; + } + + struct DeserComplex<'a>(&'a mut LinearMap>); + + impl<'a, 'de> DeserializeSeed<'de> for DeserComplex<'a> { + type Value = (); + + fn deserialize>(self, d: D) -> Result { + d.deserialize_seq(self) } - if v.starts_with("super") { - state |= ModifiersState::SUPER; - v = &v[v.len().min(5)..]; + } + + impl<'a, 'de> Visitor<'de> for DeserComplex<'a> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a sequence") } - 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.", - )) + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + while let Some(rule) = seq.next_element::()? { + let ModifiersStateDeser(state) = rule.mods; + let sub = self.0.entry(state).or_insert_with(Default::default); + sub.insert(rule.key.into(), rule.cmd); + } + + Ok(()) } } -} -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for ModifierStateVisitor { - fn deserialize(d: D) -> Result - where - D: Deserializer<'de>, - { - d.deserialize_str(ModifierStateVisitor(Default::default())) - } -} + struct ShortcutsVisitor; -#[cfg(feature = "serde")] -struct ShortcutsVisitor; -#[cfg(feature = "serde")] -impl<'de> Visitor<'de> for ShortcutsVisitor { - type Value = Shortcuts; + impl<'de> Visitor<'de> for ShortcutsVisitor { + type Value = Shortcuts; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("{ : { : } }") - } + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map") + } - 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); + fn visit_map(self, mut reader: A) -> Result + where + A: MapAccess<'de>, + { + let mut map = LinearMap::>::new(); + while let Some(opt_state) = reader.next_key::()? { + match opt_state { + OptModifiersStateDeser::State(ModifiersStateDeser(state)) => { + let sub = map.entry(state).or_insert_with(Default::default); + reader.next_value_seed(DeserSimple(sub))?; + } + OptModifiersStateDeser::Other => { + reader.next_value_seed(DeserComplex(&mut map))?; + } + } + } + + Ok(Shortcuts { map }) } - 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) + impl<'de> Deserialize<'de> for Shortcuts { + fn deserialize>(d: D) -> Result { + d.deserialize_map(ShortcutsVisitor) + } } } diff --git a/crates/kas-core/src/event/cx/config.rs b/crates/kas-core/src/event/cx/config.rs index 733bac3b4..aee18910c 100644 --- a/crates/kas-core/src/event/cx/config.rs +++ b/crates/kas-core/src/event/cx/config.rs @@ -20,7 +20,7 @@ use std::ops::{Deref, DerefMut}; /// /// This type supports easy access to [`EventState`] (via [`Deref`], /// [`DerefMut`] and [`Self::ev_state`]) as well as [`SizeCx`] -/// ([`Self::size_cx`]) and [`DrawShared`] ([`Self::draw_shared`]). +/// ([`Self::size_cx`]). #[must_use] pub struct ConfigCx<'a> { sh: &'a dyn ThemeSize, diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index 91014e362..5f2f9b633 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -374,9 +374,9 @@ impl EventState { /// redrawn automatically. /// /// Note that keyboard shortcuts and mnemonics should usually match against - /// the "logical key". [`KeyCode`] is used here since the the logical key + /// the "logical key". [`PhysicalKey`] is used here since the the logical key /// may be changed by modifier keys. - pub fn depress_with_key(&mut self, id: Id, code: KeyCode) { + pub fn depress_with_key(&mut self, id: Id, code: PhysicalKey) { if self.key_depress.values().any(|v| *v == id) { return; } @@ -554,7 +554,7 @@ impl EventState { /// cases, calling this method may be ineffective. The cursor is /// automatically "unset" when the widget is no longer hovered. /// - /// See also [`Self::update_grab_cursor`]: if a mouse grab + /// See also [`EventCx::set_grab_cursor`]: if a mouse grab /// ([`Press::grab`]) is active, its icon takes precedence. pub fn set_hover_cursor(&mut self, icon: CursorIcon) { // Note: this is acted on by EventState::update @@ -571,8 +571,6 @@ impl EventState { /// pushed to the message stack as if it were pushed with [`EventCx::push`] /// from widget `id`, allowing this widget or any ancestor to handle it in /// [`Events::handle_messages`]. - // - // TODO: Can we identify the calling widget `id` via the context (EventCx)? pub fn push_async(&mut self, id: Id, fut: Fut) where Fut: IntoFuture + 'static, diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index 1bce5360c..af61edfc5 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -52,7 +52,8 @@ pub enum GrabMode { } impl GrabMode { - fn is_pan(self) -> bool { + /// True for "pan" variants + pub fn is_pan(self) -> bool { use GrabMode::*; matches!(self, PanFull | PanScale | PanRotate | PanOnly) } @@ -195,7 +196,7 @@ pub struct EventState { hover: Option, hover_icon: CursorIcon, old_hover_icon: CursorIcon, - key_depress: LinearMap, + key_depress: LinearMap, last_mouse_coord: Coord, last_click_button: MouseButton, last_click_repetitions: u32, @@ -368,7 +369,7 @@ impl<'a> DerefMut for EventCx<'a> { /// Internal methods impl<'a> EventCx<'a> { - fn start_key_event(&mut self, mut widget: Node<'_>, vkey: Key, code: KeyCode) { + fn start_key_event(&mut self, mut widget: Node<'_>, vkey: Key, code: PhysicalKey) { log::trace!( "start_key_event: widget={}, vkey={vkey:?}, physical_key={code:?}", widget.id() @@ -458,10 +459,10 @@ impl<'a> EventCx<'a> { } let event = Event::Command(Command::Activate, Some(code)); self.send_event(widget, id, event); - } else if self.config.nav_focus && vkey == Key::Tab { + } else if self.config.nav_focus && vkey == Key::Named(NamedKey::Tab) { let shift = self.modifiers.shift_key(); self.next_nav_focus_impl(widget.re(), None, shift, FocusSource::Key); - } else if vkey == Key::Escape { + } else if vkey == Key::Named(NamedKey::Escape) { if let Some(id) = self.popups.last().map(|(id, _, _)| *id) { self.close_window(id); } diff --git a/crates/kas-core/src/event/events.rs b/crates/kas-core/src/event/events.rs index de0392b2d..fc13bbfe6 100644 --- a/crates/kas-core/src/event/events.rs +++ b/crates/kas-core/src/event/events.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use super::{EventCx, IsUsed, Unused, Used}; #[allow(unused)] use super::{EventState, GrabMode}; -use super::{Key, KeyCode, KeyEvent, Press}; +use super::{Key, KeyEvent, NamedKey, PhysicalKey, Press}; use crate::geom::{DVec2, Offset}; use crate::{dir::Direction, Id, WindowId}; #[allow(unused)] use crate::{Events, Popup}; @@ -27,14 +27,14 @@ pub enum Event { /// A generic "command". The source is often but not always a key press. /// In many cases (but not all) the target widget has navigation focus. /// - /// A [`KeyCode`] is attached when the command is caused by a key press. + /// A [`PhysicalKey`] is attached when the command is caused by a key press. /// The recipient may use this to call [`EventState::depress_with_key`]. /// /// If a widget has keyboard input focus (see /// [`EventState::request_key_focus`]) it will instead receive /// [`Event::Key`] for key presses (but may still receive `Event::Command` /// from other sources). - Command(Command, Option), + Command(Command, Option), /// Keyboard input: `event, is_synthetic` /// /// This is only received by a widget with character focus (see @@ -346,7 +346,7 @@ impl Event { /// *Most* `Command` entries represent an action (such as `Copy` or `FindNext`) /// but some represent an important key whose action may be context-dependent /// (e.g. `Escape`, `Space`). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] pub enum Command { @@ -515,48 +515,51 @@ pub enum Command { impl Command { /// Try constructing from a [`winit::keyboard::Key`] pub fn new(key: &Key) -> Option { - Some(match key { - Key::ScrollLock => Command::ScrollLock, - Key::Enter => Command::Enter, - Key::Tab => Command::Tab, - Key::Space => Command::Space, - Key::ArrowDown => Command::Down, - Key::ArrowLeft => Command::Left, - Key::ArrowRight => Command::Right, - Key::ArrowUp => Command::Up, - Key::End => Command::End, - Key::Home => Command::Home, - Key::PageDown => Command::PageDown, - Key::PageUp => Command::PageUp, - Key::Backspace => Command::DelBack, - Key::Clear => Command::Deselect, - Key::Copy => Command::Copy, - Key::Cut => Command::Cut, - Key::Delete => Command::Delete, - Key::Insert => Command::Insert, - Key::Paste => Command::Paste, - Key::Redo | Key::Again => Command::Redo, - Key::Undo => Command::Undo, - Key::ContextMenu => Command::ContextMenu, - Key::Escape => Command::Escape, - Key::Execute => Command::Activate, - Key::Find => Command::Find, - Key::Help => Command::Help, - Key::Pause => Command::Pause, - Key::Select => Command::SelectAll, - Key::PrintScreen => Command::Snapshot, - // Key::Close => CloseDocument ? - Key::New => Command::New, - Key::Open => Command::Open, - Key::Print => Command::Print, - Key::Save => Command::Save, - Key::SpellCheck => Command::SpellCheck, - Key::BrowserBack | Key::GoBack => Command::NavPrevious, - Key::BrowserForward => Command::NavNext, - Key::BrowserRefresh => Command::Refresh, - Key::Exit => Command::Exit, - _ => return None, - }) + match key { + Key::Named(named) => Some(match named { + NamedKey::ScrollLock => Command::ScrollLock, + NamedKey::Enter => Command::Enter, + NamedKey::Tab => Command::Tab, + NamedKey::Space => Command::Space, + NamedKey::ArrowDown => Command::Down, + NamedKey::ArrowLeft => Command::Left, + NamedKey::ArrowRight => Command::Right, + NamedKey::ArrowUp => Command::Up, + NamedKey::End => Command::End, + NamedKey::Home => Command::Home, + NamedKey::PageDown => Command::PageDown, + NamedKey::PageUp => Command::PageUp, + NamedKey::Backspace => Command::DelBack, + NamedKey::Clear => Command::Deselect, + NamedKey::Copy => Command::Copy, + NamedKey::Cut => Command::Cut, + NamedKey::Delete => Command::Delete, + NamedKey::Insert => Command::Insert, + NamedKey::Paste => Command::Paste, + NamedKey::Redo | NamedKey::Again => Command::Redo, + NamedKey::Undo => Command::Undo, + NamedKey::ContextMenu => Command::ContextMenu, + NamedKey::Escape => Command::Escape, + NamedKey::Execute => Command::Activate, + NamedKey::Find => Command::Find, + NamedKey::Help => Command::Help, + NamedKey::Pause => Command::Pause, + NamedKey::Select => Command::SelectAll, + NamedKey::PrintScreen => Command::Snapshot, + // NamedKey::Close => CloseDocument ? + NamedKey::New => Command::New, + NamedKey::Open => Command::Open, + NamedKey::Print => Command::Print, + NamedKey::Save => Command::Save, + NamedKey::SpellCheck => Command::SpellCheck, + NamedKey::BrowserBack | NamedKey::GoBack => Command::NavPrevious, + NamedKey::BrowserForward => Command::NavNext, + NamedKey::BrowserRefresh => Command::Refresh, + NamedKey::Exit => Command::Exit, + _ => return None, + }), + _ => None, + } } /// True for "activation" commands diff --git a/crates/kas-core/src/event/mod.rs b/crates/kas-core/src/event/mod.rs index 8ac4fc539..64172bd7e 100644 --- a/crates/kas-core/src/event/mod.rs +++ b/crates/kas-core/src/event/mod.rs @@ -12,7 +12,7 @@ //! An [`Id`] represents a *path* and may be used to find the most //! direct root from the root to the target. //! -//! An [`Event`] is [sent](EventCx::send) to a target widget as follows: +//! An [`Event`] is sent to a target widget as follows: //! //! 1. Determine the target's [`Id`]. For example, this may be //! the [`nav_focus`](EventState::nav_focus) or may be determined from @@ -68,7 +68,7 @@ pub use smol_str::SmolStr; #[cfg(winit)] pub use winit::event::{ElementState, KeyEvent, MouseButton}; #[cfg(winit)] -pub use winit::keyboard::{Key, KeyCode, ModifiersState}; +pub use winit::keyboard::{Key, ModifiersState, NamedKey, PhysicalKey}; #[cfg(winit)] pub use winit::window::{CursorIcon, ResizeDirection}; // used by Key diff --git a/crates/kas-core/src/message.rs b/crates/kas-core/src/message.rs index 1102bce36..4f8386257 100644 --- a/crates/kas-core/src/message.rs +++ b/crates/kas-core/src/message.rs @@ -7,7 +7,7 @@ //! //! These are messages that may be sent via [`EventCx::push`](crate::event::EventCx::push). -use crate::event::KeyCode; +use crate::event::PhysicalKey; /// Message: activate /// @@ -16,7 +16,7 @@ use crate::event::KeyCode; /// /// Payload: the key press which caused this message to be emitted, if any. #[derive(Copy, Clone, Debug)] -pub struct Activate(pub Option); +pub struct Activate(pub Option); /// Message: select child /// diff --git a/crates/kas-core/src/shell/common.rs b/crates/kas-core/src/shell/common.rs index fe277689a..8e2bac277 100644 --- a/crates/kas-core/src/shell/common.rs +++ b/crates/kas-core/src/shell/common.rs @@ -5,9 +5,10 @@ //! Public shell stuff common to all backends +use crate::draw::DrawSharedImpl; use crate::draw::{color::Rgba, DrawIface, WindowCommon}; -use crate::draw::{DrawImpl, DrawSharedImpl}; use crate::geom::Size; +use crate::theme::Theme; use raw_window_handle as raw; use thiserror::Error; @@ -172,12 +173,12 @@ impl Platform { #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] pub trait GraphicalShell { + /// The default theme + type DefaultTheme: Default + Theme; + /// Shared draw state type Shared: DrawSharedImpl; - /// Per-window draw state - type Window: DrawImpl; - /// Window surface type Surface: WindowSurface + 'static; diff --git a/crates/kas-core/src/shell/event_loop.rs b/crates/kas-core/src/shell/event_loop.rs index b0aae0735..4657a7a80 100644 --- a/crates/kas-core/src/shell/event_loop.rs +++ b/crates/kas-core/src/shell/event_loop.rs @@ -171,6 +171,8 @@ where Event::LoopExiting => { self.shared.on_exit(); } + + Event::MemoryWarning => (), // TODO ? } } diff --git a/crates/kas-core/src/shell/mod.rs b/crates/kas-core/src/shell/mod.rs index 0ffdfc31f..e6af7855b 100644 --- a/crates/kas-core/src/shell/mod.rs +++ b/crates/kas-core/src/shell/mod.rs @@ -23,7 +23,7 @@ pub use common::{Error, Platform, Result}; #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] pub use common::{GraphicalShell, WindowSurface}; #[cfg(winit)] -pub use shell::{ClosedError, Proxy, Shell, ShellAssoc}; +pub use shell::{ClosedError, Proxy, Shell, ShellAssoc, ShellBuilder}; #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] pub extern crate raw_window_handle; diff --git a/crates/kas-core/src/shell/shared.rs b/crates/kas-core/src/shell/shared.rs index 059d5f615..22227cbb4 100644 --- a/crates/kas-core/src/shell/shared.rs +++ b/crates/kas-core/src/shell/shared.rs @@ -227,7 +227,7 @@ impl> ShellSharedErased // handled to create the winit window here or use statics to generate // errors now, but user code can't do much with this error anyway. let id = self.next_window_id(); - let window = Box::new(super::Window::new(&self, id, window)); + let window = Box::new(super::Window::new(self, id, window)); self.pending.push_back(Pending::AddWindow(id, window)); id } diff --git a/crates/kas-core/src/shell/shell.rs b/crates/kas-core/src/shell/shell.rs index 51fcb9f19..cc20246fb 100644 --- a/crates/kas-core/src/shell/shell.rs +++ b/crates/kas-core/src/shell/shell.rs @@ -7,11 +7,11 @@ use super::{GraphicalShell, Platform, ProxyAction, Result, SharedState}; use crate::config::Options; -use crate::draw::{DrawImpl, DrawShared, DrawSharedImpl}; +use crate::draw::{DrawShared, DrawSharedImpl}; use crate::event; use crate::theme::{self, Theme, ThemeConfig}; use crate::util::warn_about_error; -use crate::{AppData, Window, WindowId}; +use crate::{impl_scope, AppData, Window, WindowId}; use std::cell::RefCell; use std::rc::Rc; use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy}; @@ -19,27 +19,99 @@ use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy}; /// The KAS shell /// /// The "shell" is the layer over widgets, windows, events and graphics. -/// -/// Constructing with [`Shell::new`] or [`Shell::new_custom`] -/// reads configuration (depending on passed options or environment variables) -/// and initialises the font database. Note that this database is a global -/// singleton and some widgets and other library code may expect fonts to have -/// been initialised first. pub struct Shell> { el: EventLoop, windows: Vec>>, shared: SharedState, } +impl_scope! { + pub struct ShellBuilder> { + graphical: G, + theme: T, + options: Option, + config: Option>>, + } + + impl Self { + /// Construct from a graphical shell and a theme + #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] + #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] + pub fn new(graphical: G, theme: T) -> Self { + ShellBuilder { + graphical, + theme, + options: None, + config: None, + } + } + + /// Use the specified `options` + /// + /// If omitted, options are provided by [`Options::from_env`]. + #[inline] + pub fn with_options(mut self, options: Options) -> Self { + self.options = Some(options); + self + } + + /// Use the specified event `config` + /// + /// This is a wrapper around [`Self::with_event_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))) + } + + /// 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 { + 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)), + Err(error) => { + warn_about_error("Shell::new_custom: failed to read config", &error); + Default::default() + } + }); + + let el = EventLoopBuilder::with_user_event().build()?; + + let mut draw_shared = self.graphical.build()?; + draw_shared.set_raster_config(theme.config().raster()); + + let pw = PlatformWrapper(&el); + let shared = SharedState::new(data, pw, draw_shared, theme, options, config)?; + + Ok(Shell { + el, + windows: vec![], + shared, + }) + } + } +} + /// Shell associated types /// /// Note: these could be inherent associated types of [`Shell`] when Rust#8995 is stable. pub trait ShellAssoc { /// Shared draw state type type DrawShared: DrawSharedImpl; - - /// Per-window draw state - type Draw: DrawImpl; } impl ShellAssoc for Shell @@ -48,17 +120,13 @@ where T::Window: theme::Window, { type DrawShared = G::Shared; - - type Draw = G::Window; } -impl Shell +impl Shell where G: GraphicalShell + Default, - T: Theme + 'static, - T::Window: theme::Window, { - /// Construct a new instance with default options. + /// Construct a new instance with default options and theme /// /// All user interfaces are expected to provide `data: Data`: widget data /// shared across all windows. If not required this may be `()`. @@ -67,73 +135,34 @@ where /// of [`Options::from_env`]. KAS config is provided by /// [`Options::read_config`]. #[inline] - pub fn new(data: Data, theme: T) -> Result { - Self::new_custom(data, G::default(), theme, Options::from_env()) + pub fn new(data: Data) -> Result { + Self::with_default_theme().build(data) } -} -impl Shell -where - T: Theme + 'static, - T::Window: theme::Window, -{ - /// Construct an instance with custom options - /// - /// The [`Options`] parameter allows direct specification of shell options; - /// usually, these are provided by [`Options::from_env`]. - /// - /// KAS config is provided by [`Options::read_config`] and `theme` is - /// configured through [`Options::init_theme_config`]. + /// Construct a builder with the default theme #[inline] - pub fn new_custom( - data: Data, - graphical_shell: impl Into, - mut theme: T, - options: Options, - ) -> Result { - options.init_theme_config(&mut theme)?; - let config = match options.read_config() { - Ok(config) => config, - Err(error) => { - warn_about_error("Shell::new_custom: failed to read config", &error); - Default::default() - } - }; - let config = Rc::new(RefCell::new(config)); - - Self::new_custom_config(data, graphical_shell, theme, options, config) + pub fn with_default_theme() -> ShellBuilder { + ShellBuilder::new(G::default(), G::DefaultTheme::default()) } +} - /// Construct an instance with custom options and config - /// - /// This is like [`Shell::new_custom`], but allows KAS config to be - /// specified directly, instead of loading via [`Options::read_config`]. - /// - /// Unlike other the constructors, this method does not configure the theme. - /// The user should call [`Options::init_theme_config`] before this method. +impl Shell<(), G, T> +where + G: GraphicalShell + Default, + T: Theme, +{ + /// Construct a builder with the given `theme` #[inline] - pub fn new_custom_config( - data: Data, - graphical_shell: impl Into, - theme: T, - options: Options, - config: Rc>, - ) -> Result { - let el = EventLoopBuilder::with_user_event().build()?; - let windows = vec![]; - - let mut draw_shared = graphical_shell.into().build()?; - draw_shared.set_raster_config(theme.config().raster()); - let pw = PlatformWrapper(&el); - let shared = SharedState::new(data, pw, draw_shared, theme, options, config)?; - - Ok(Shell { - el, - windows, - shared, - }) + pub fn with_theme(theme: T) -> ShellBuilder { + ShellBuilder::new(G::default(), theme) } +} +impl Shell +where + T: Theme + 'static, + T::Window: theme::Window, +{ /// Access shared draw state #[inline] pub fn draw_shared(&mut self) -> &mut dyn DrawShared { diff --git a/crates/kas-core/src/shell/window.rs b/crates/kas-core/src/shell/window.rs index ac9a54b3b..c117b331f 100644 --- a/crates/kas-core/src/shell/window.rs +++ b/crates/kas-core/src/shell/window.rs @@ -149,9 +149,7 @@ impl> Window { } #[cfg(all(wayland_platform, feature = "clipboard"))] - use winit::window::raw_window_handle::{ - HasRawDisplayHandle, RawDisplayHandle, WaylandDisplayHandle, - }; + use raw_window_handle::{HasRawDisplayHandle, RawDisplayHandle, WaylandDisplayHandle}; #[cfg(all(wayland_platform, feature = "clipboard"))] let wayland_clipboard = match window.raw_display_handle() { RawDisplayHandle::Wayland(WaylandDisplayHandle { display, .. }) => { @@ -284,7 +282,7 @@ impl> Window { } /// Handle an action (excludes handling of CLOSE and EXIT) - pub(super) fn handle_action(&mut self, shared: &mut SharedState, mut action: Action) { + pub(super) fn handle_action(&mut self, shared: &SharedState, 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; @@ -375,7 +373,7 @@ impl> Window { // Internal functions impl> Window { - fn reconfigure(&mut self, shared: &mut SharedState) { + fn reconfigure(&mut self, shared: &SharedState) { let time = Instant::now(); let Some(ref mut window) = self.window else { return; @@ -391,7 +389,7 @@ impl> Window { log::trace!(target: "kas_perf::wgpu::window", "reconfigure: {}µs", time.elapsed().as_micros()); } - fn update(&mut self, shared: &mut SharedState) { + fn update(&mut self, shared: &SharedState) { let time = Instant::now(); let Some(ref mut window) = self.window else { return; @@ -404,7 +402,7 @@ impl> Window { log::trace!(target: "kas_perf::wgpu::window", "update: {}µs", time.elapsed().as_micros()); } - fn apply_size(&mut self, shared: &mut SharedState, first: bool) { + fn apply_size(&mut self, shared: &SharedState, first: bool) { let time = Instant::now(); let Some(ref mut window) = self.window else { return; diff --git a/crates/kas-resvg/src/svg.rs b/crates/kas-resvg/src/svg.rs index fe55b49c9..a18da17eb 100644 --- a/crates/kas-resvg/src/svg.rs +++ b/crates/kas-resvg/src/svg.rs @@ -242,7 +242,7 @@ impl_scope! { let size: (u32, u32) = self.core.rect.size.cast(); if let Some(fut) = self.inner.resize(size) { - cx.ev_state().push_spawn(self.id(), fut); + cx.push_spawn(self.id(), fut); } } diff --git a/crates/kas-wgpu/src/lib.rs b/crates/kas-wgpu/src/lib.rs index 8d8430d60..2de7cd6de 100644 --- a/crates/kas-wgpu/src/lib.rs +++ b/crates/kas-wgpu/src/lib.rs @@ -14,7 +14,7 @@ //! (see the [Mandlebrot example](https://github.com/kas-gui/kas/blob/master/kas-wgpu/examples/mandlebrot.rs)). //! //! By default, some environment variables are read for configuration. -//! See [`options::Options::from_env`] for documentation. +//! See [`options::Options::load_from_env`] for documentation. //! //! [WGPU]: https://github.com/gfx-rs/wgpu @@ -27,7 +27,8 @@ mod shaded_theme; mod surface; use crate::draw::{CustomPipeBuilder, DrawPipe}; -use kas::shell::{GraphicalShell, Result}; +use kas::shell::{GraphicalShell, Result, ShellBuilder}; +use kas::theme::{FlatTheme, Theme}; pub use draw_shaded::{DrawShaded, DrawShadedImpl}; pub use options::Options; @@ -35,29 +36,76 @@ pub use shaded_theme::ShadedTheme; pub extern crate wgpu; /// Builder for a KAS shell using WGPU -pub struct WgpuShellBuilder(CB, Options); +pub struct WgpuBuilder { + custom: CB, + options: Options, + read_env_vars: bool, +} + +impl GraphicalShell for WgpuBuilder { + type DefaultTheme = FlatTheme; -impl GraphicalShell for WgpuShellBuilder { type Shared = DrawPipe; - type Window = draw::DrawWindow<::Window>; + type Surface = surface::Surface; fn build(self) -> Result { - DrawPipe::new(self.0, &self.1) + let mut options = self.options; + if self.read_env_vars { + options.load_from_env(); + } + DrawPipe::new(self.custom, &options) } } -impl Default for WgpuShellBuilder<()> { +impl Default for WgpuBuilder<()> { fn default() -> Self { - WgpuShellBuilder((), Options::from_env()) + WgpuBuilder::new(()) } } -impl From for WgpuShellBuilder { - fn from(cb: CB) -> Self { - WgpuShellBuilder(cb, Options::from_env()) +impl WgpuBuilder { + /// Construct with the given pipe builder + /// + /// Pass `()` or use [`Self::default`] when not using a custom pipe. + #[inline] + pub fn new(cb: CB) -> Self { + WgpuBuilder { + custom: cb, + options: Options::default(), + read_env_vars: true, + } + } + + /// Specify the default WGPU options + /// + /// These options serve as a default, but may still be replaced by values + /// read from env vars unless disabled via [`Self::read_env_vars`]. + #[inline] + pub fn with_wgpu_options(mut self, options: Options) -> Self { + self.options = options; + self } -} -/// The default (unparameterised) implementation of [`WgpuShellBuilder`] -pub type DefaultGraphicalShell = WgpuShellBuilder<()>; + /// En/dis-able reading options from environment variables + /// + /// Default: `true`. If enabled, options will be read from env vars where + /// present (see [`Options::load_from_env`]). + #[inline] + pub fn read_env_vars(mut self, read_env_vars: bool) -> Self { + self.read_env_vars = read_env_vars; + self + } + + /// Convert to a [`ShellBuilder`] using the default theme + #[inline] + pub fn with_default_theme(self) -> ShellBuilder { + ShellBuilder::new(self, FlatTheme::new()) + } + + /// Convert to a [`ShellBuilder`] using the specified `theme` + #[inline] + pub fn with_theme>>(self, theme: T) -> ShellBuilder { + ShellBuilder::new(self, theme) + } +} diff --git a/crates/kas-wgpu/src/options.rs b/crates/kas-wgpu/src/options.rs index 6fb675f0d..12ca0af29 100644 --- a/crates/kas-wgpu/src/options.rs +++ b/crates/kas-wgpu/src/options.rs @@ -31,7 +31,10 @@ impl Default for Options { } impl Options { - /// Construct a new instance, reading from environment variables + /// Read values from environment variables + /// + /// This replaces values in self where specified via env vars. + /// Use e.g. `Options::default().load_from_env()`. /// /// The following environment variables are read, in case-insensitive mode. /// @@ -62,12 +65,10 @@ impl Options { /// ``` /// /// [API tracing]: https://github.com/gfx-rs/wgpu/wiki/Debugging-wgpu-Applications#tracing-infrastructure - pub fn from_env() -> Self { - let mut options = Options::default(); - + pub fn load_from_env(&mut self) { if let Ok(mut v) = var("KAS_POWER_PREFERENCE") { v.make_ascii_uppercase(); - options.power_preference = match v.as_str() { + self.power_preference = match v.as_str() { "DEFAULT" | "LOWPOWER" => PowerPreference::LowPower, "HIGHPERFORMANCE" => PowerPreference::HighPerformance, other => { @@ -75,14 +76,14 @@ impl Options { log::error!( "from_env: supported power modes: DEFAULT, LOWPOWER, HIGHPERFORMANCE" ); - options.power_preference + self.power_preference } } } if let Ok(mut v) = var("KAS_BACKENDS") { v.make_ascii_uppercase(); - options.backends = match v.as_str() { + self.backends = match v.as_str() { "VULKAN" => Backends::VULKAN, "GL" => Backends::GL, "METAL" => Backends::METAL, @@ -95,16 +96,14 @@ impl Options { other => { log::error!("from_env: bad var KAS_BACKENDS={other}"); log::error!("from_env: supported backends: VULKAN, GL, METAL, DX11, DX12, BROWSER_WEBGPU, PRIMARY, SECONDARY, FALLBACK"); - options.backends + self.backends } } } if let Ok(v) = var("KAS_WGPU_TRACE_PATH") { - options.wgpu_trace_path = Some(v.into()); + self.wgpu_trace_path = Some(v.into()); } - - options } pub(crate) fn adapter_options(&self) -> wgpu::RequestAdapterOptions { diff --git a/crates/kas-widgets/src/dialog.rs b/crates/kas-widgets/src/dialog.rs index 4a8f9c3a1..a1d53fe77 100644 --- a/crates/kas-widgets/src/dialog.rs +++ b/crates/kas-widgets/src/dialog.rs @@ -14,7 +14,7 @@ //! and their design is likely to change. use crate::{adapt::AdaptWidgetAny, Button, EditBox, Filler, Label}; -use kas::event::{Command, Key}; +use kas::event::{Command, NamedKey}; use kas::prelude::*; use kas::text::format::FormattableText; @@ -40,7 +40,8 @@ impl_scope! { MessageBox { core: Default::default(), label: Label::new(message), - button: Button::new_msg(Label::new("Ok"), MessageBoxOk).with_access_key(Key::Enter), + button: Button::new_msg(Label::new("Ok"), MessageBoxOk) + .with_access_key(NamedKey::Enter.into()), } } diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index c3ccbb35c..1050cd338 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -7,7 +7,9 @@ use crate::{ScrollBar, ScrollMsg}; use kas::event::components::{TextInput, TextInputAction}; -use kas::event::{Command, CursorIcon, ElementState, FocusSource, KeyCode, Scroll, ScrollDelta}; +use kas::event::{ + Command, CursorIcon, ElementState, FocusSource, PhysicalKey, Scroll, ScrollDelta, +}; use kas::geom::Vec2; use kas::prelude::*; use kas::text::{NotReady, SelectionHelper, Text}; @@ -1132,7 +1134,7 @@ impl EditField { cx: &mut EventCx, data: &G::Data, cmd: Command, - code: Option, + code: Option, ) -> Result { let editable = self.editable; let mut shift = cx.modifiers().shift_key(); diff --git a/examples/calculator.rs b/examples/calculator.rs index e2651c7bc..0ceef22b5 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -8,6 +8,7 @@ use std::num::ParseFloatError; use std::str::FromStr; +use kas::event::NamedKey; use kas::prelude::*; use kas::widgets::{AccessLabel, Adapt, Button, EditBox}; @@ -32,10 +33,11 @@ fn calc_ui() -> Window<()> { // We use map_any to avoid passing input data (not wanted by buttons): let buttons = kas::grid! { // Key bindings: C, Del - (0, 0) => Button::label_msg("&clear", Key::Clear).with_access_key(Key::Delete), + (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear)) + .with_access_key(NamedKey::Delete.into()), // Widget is hidden but has key binding. // TODO(opt): exclude from layout & drawing. - (0, 0) => key_button_with("", Key::Backspace), + (0, 0) => key_button_with("", NamedKey::Backspace.into()), (1, 0) => key_button_with("&÷", Key::Character("/".into())), (2, 0) => key_button_with("&×", Key::Character("*".into())), (3, 0) => key_button_with("&−", Key::Character("-".into())), @@ -49,7 +51,7 @@ fn calc_ui() -> Window<()> { (0, 3) => key_button("&1"), (1, 3) => key_button("&2"), (2, 3) => key_button("&3"), - (3, 3..5) => key_button_with("&=", Key::Enter), + (3, 3..5) => key_button_with("&=", NamedKey::Enter.into()), (0..2, 4) => key_button("&0"), (2, 4) => key_button("&."), } @@ -71,7 +73,8 @@ fn main() -> kas::shell::Result<()> { env_logger::init(); let theme = kas_wgpu::ShadedTheme::new().with_font_size(16.0); - kas::shell::DefaultShell::new((), theme)? + kas::shell::Default::with_theme(theme) + .build(())? .with(calc_ui()) .run() } @@ -129,19 +132,19 @@ impl Calculator { fn handle(&mut self, key: Key) { match key { - Key::Clear | Key::Delete => { + Key::Named(NamedKey::Clear) | Key::Named(NamedKey::Delete) => { self.state = Ok(0.0); self.op = Op::None; self.line_buf.clear(); } - Key::Backspace => { + Key::Named(NamedKey::Backspace) => { self.line_buf.pop(); } Key::Character(s) if s == "/" => self.do_op(Op::Divide), Key::Character(s) if s == "*" => self.do_op(Op::Multiply), Key::Character(s) if s == "-" => self.do_op(Op::Subtract), Key::Character(s) if s == "+" => self.do_op(Op::Add), - Key::Enter => self.do_op(Op::None), + Key::Named(NamedKey::Enter) => self.do_op(Op::None), Key::Character(s) if s.len() == 1 => { let c = s.chars().next().unwrap(); if ('0'..='9').contains(&c) || c == '.' { diff --git a/examples/clock.rs b/examples/clock.rs index 619d7f35e..29534958a 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -25,8 +25,7 @@ use kas::prelude::*; use kas::shell::ShellAssoc; use kas::text::Text; -type Theme = kas::theme::FlatTheme; -type Shell = kas::shell::DefaultShell<(), Theme>; +type Shell = kas::shell::Default<(), kas::theme::FlatTheme>; impl_scope! { #[derive(Clone)] @@ -179,5 +178,5 @@ fn main() -> kas::shell::Result<()> { .with_decorations(kas::Decorations::None) .with_transparent(true); - Shell::new((), Theme::new())?.with(window).run() + Shell::new(())?.with(window).run() } diff --git a/examples/counter.rs b/examples/counter.rs index d0f4fe5bc..4a4b8e39c 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -28,7 +28,8 @@ fn main() -> kas::shell::Result<()> { env_logger::init(); let theme = kas::theme::SimpleTheme::new().with_font_size(24.0); - kas::shell::DefaultShell::new((), theme)? + kas::shell::Default::with_theme(theme) + .build(())? .with(Window::new(counter(), "Counter")) .run() } diff --git a/examples/cursors.rs b/examples/cursors.rs index 775b20804..ccac6185f 100644 --- a/examples/cursors.rs +++ b/examples/cursors.rs @@ -82,6 +82,5 @@ fn main() -> kas::shell::Result<()> { ]); let window = Window::new(column, "Cursor gallery"); - let theme = kas::theme::FlatTheme::new(); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::new(())?.with(window).run() } diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index 91c0c403d..aaece0ec4 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -243,6 +243,5 @@ fn main() -> kas::shell::Result<()> { let window = Window::new(ui, "Dynamic widget demo"); - let theme = kas::theme::FlatTheme::new(); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::new(())?.with(window).run() } diff --git a/examples/data-list.rs b/examples/data-list.rs index 906e859cf..d1f3c9d23 100644 --- a/examples/data-list.rs +++ b/examples/data-list.rs @@ -185,6 +185,5 @@ fn main() -> kas::shell::Result<()> { let window = Window::new(ui, "Dynamic widget demo"); - let theme = kas::theme::FlatTheme::new(); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::new(())?.with(window).run() } diff --git a/examples/gallery.rs b/examples/gallery.rs index 9449623e2..cfb2d3be1 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -518,7 +518,7 @@ fn main() -> kas::shell::Result<()> { .add("simple", kas::theme::SimpleTheme::new()) .add("shaded", kas_wgpu::ShadedTheme::new()) .build(); - let mut shell = kas::shell::DefaultShell::new((), theme)?; + let mut shell = kas::shell::Default::with_theme(theme).build(())?; // TODO: use as logo of tab // let img_gallery = Svg::new(include_bytes!("../res/gallery-line.svg")); diff --git a/examples/hello.rs b/examples/hello.rs index 30f91d68e..9b2c4af6f 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -10,6 +10,5 @@ use kas::widgets::dialog::MessageBox; fn main() -> kas::shell::Result<()> { let window = MessageBox::new("Message").into_window("Hello world"); - let theme = kas::theme::FlatTheme::new(); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::new(())?.with(window).run() } diff --git a/examples/layout.rs b/examples/layout.rs index b821a83ef..f906c1cd3 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -24,6 +24,5 @@ fn main() -> kas::shell::Result<()> { }; let window = Window::new(ui, "Layout demo"); - let theme = kas::theme::FlatTheme::new(); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::new(())?.with(window).run() } diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs index 754cc8171..7f21c483d 100644 --- a/examples/mandlebrot/mandlebrot.rs +++ b/examples/mandlebrot/mandlebrot.rs @@ -485,8 +485,9 @@ fn main() -> kas::shell::Result<()> { let window = Window::new(MandlebrotUI::new(), "Mandlebrot"); let theme = kas::theme::FlatTheme::new().with_colours("dark"); - let options = kas::config::Options::from_env(); - kas::shell::WgpuShell::new_custom((), PipeBuilder, theme, options)? + kas::shell::WgpuBuilder::new(PipeBuilder) + .with_theme(theme) + .build(())? .with(window) .run() } diff --git a/examples/proxy.rs b/examples/proxy.rs index b282d5a79..31a4a96e6 100644 --- a/examples/proxy.rs +++ b/examples/proxy.rs @@ -39,8 +39,7 @@ fn main() -> kas::shell::Result<()> { env_logger::init(); let data = AppData { color: None }; - let theme = kas::theme::FlatTheme::new(); - let shell = kas::shell::DefaultShell::new(data, theme)?; + let shell = kas::shell::Default::new(data)?; // We construct a proxy from the shell to enable cross-thread communication. let proxy = shell.create_proxy(); diff --git a/examples/splitter.rs b/examples/splitter.rs index 74780132f..fafc24468 100644 --- a/examples/splitter.rs +++ b/examples/splitter.rs @@ -38,5 +38,8 @@ fn main() -> kas::shell::Result<()> { let window = Window::new(adapt, "Slitter panes"); let theme = kas_wgpu::ShadedTheme::new(); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::with_theme(theme) + .build(())? + .with(window) + .run() } diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index 78168a74f..394381f2b 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -83,5 +83,8 @@ fn main() -> kas::shell::Result<()> { let theme = kas_wgpu::ShadedTheme::new() .with_colours("dark") .with_font_size(18.0); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::with_theme(theme) + .build(())? + .with(window) + .run() } diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs index b8d1606b1..134f4f6f6 100644 --- a/examples/sync-counter.rs +++ b/examples/sync-counter.rs @@ -62,7 +62,8 @@ fn main() -> kas::shell::Result<()> { let count = Count(0); let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0); - kas::shell::DefaultShell::new(count, theme)? + kas::shell::Default::with_theme(theme) + .build(count)? .with(counter("Counter 1")) .with(counter("Counter 2")) .run() diff --git a/examples/times-tables.rs b/examples/times-tables.rs index 057956e04..e6fd2983b 100644 --- a/examples/times-tables.rs +++ b/examples/times-tables.rs @@ -76,5 +76,8 @@ fn main() -> kas::shell::Result<()> { let window = Window::new(ui, "Times-Tables"); let theme = kas::theme::SimpleTheme::new().with_font_size(16.0); - kas::shell::DefaultShell::new((), theme)?.with(window).run() + kas::shell::Default::with_theme(theme) + .build(())? + .with(window) + .run() } diff --git a/src/lib.rs b/src/lib.rs index 101ee3e91..41656e8dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,20 +74,16 @@ pub mod resvg { pub mod shell { //! Shell: window runtime environment //! - //! A [`Shell`] is used to manage a GUI. Most GUIs will use the - //! [`DefaultShell`] type-def (requires a backend be enabled, e.g. "wgpu"). + //! A [`Shell`] is used to manage a GUI. Most GUIs will use the [`Default`](type@Default) + //! shell type-def (requires a backend be enabled, e.g. "wgpu"). pub use kas_core::shell::*; - /// The WGPU shell - #[cfg(feature = "wgpu")] - pub type WgpuShell = - kas_core::shell::Shell, T>; + #[cfg(feature = "wgpu")] pub use kas_wgpu::WgpuBuilder; /// The default (configuration-specific) shell #[cfg(feature = "wgpu")] - pub type DefaultShell = - kas_core::shell::Shell; + pub type Default = kas_core::shell::Shell, T>; } #[cfg(feature = "dynamic")]