Skip to content

Commit

Permalink
Merge pull request #416 from kas-gui/work2
Browse files Browse the repository at this point in the history
ShellBuilder, config load/save tweaks, new shortcut serialization format, enable TOML
  • Loading branch information
dhardy authored Oct 23, 2023
2 parents 26fe69d + 8999157 commit 78eb7cb
Show file tree
Hide file tree
Showing 39 changed files with 769 additions and 490 deletions.
11 changes: 5 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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"]

Expand Down Expand Up @@ -146,7 +149,3 @@ members = [
"crates/kas-view",
"examples/mandlebrot",
]

[patch.crates-io.winit]
git = "https://github.com/rust-windowing/winit.git"
rev = "cb58c49a90f17734e0405627130674d47c0b8f40"
97 changes: 33 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,89 +7,58 @@ 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

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
Expand Down
7 changes: 6 additions & 1 deletion crates/kas-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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"
Expand All @@ -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"]
66 changes: 44 additions & 22 deletions crates/kas-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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),

Expand Down Expand Up @@ -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))
Expand All @@ -149,27 +165,34 @@ impl Format {
#[cfg(feature = "serde")]
pub fn write_path<T: Serialize>(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))
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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(),
}
Expand All @@ -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)
}
};
Expand All @@ -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())?;
}
}

Expand Down
11 changes: 5 additions & 6 deletions crates/kas-core/src/core/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
Loading

0 comments on commit 78eb7cb

Please sign in to comment.