Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ShellBuilder, config load/save tweaks, new shortcut serialization format, enable TOML #416

Merged
merged 15 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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