From eae17857ad7c10d97f414c5bb79e764648aa0285 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 11 Jul 2022 10:48:39 +0100 Subject: [PATCH 01/27] Rename private module kas_widgets::adapter::label to with_label --- crates/kas-widgets/src/adapter/mod.rs | 4 ++-- crates/kas-widgets/src/adapter/{label.rs => with_label.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/kas-widgets/src/adapter/{label.rs => with_label.rs} (100%) diff --git a/crates/kas-widgets/src/adapter/mod.rs b/crates/kas-widgets/src/adapter/mod.rs index 949262663..41faa910e 100644 --- a/crates/kas-widgets/src/adapter/mod.rs +++ b/crates/kas-widgets/src/adapter/mod.rs @@ -6,11 +6,11 @@ //! Adapter widgets (wrappers) mod adapt_widget; -mod label; mod map; mod reserve; +mod with_label; pub use adapt_widget::*; -pub use label::WithLabel; pub use map::MapMessage; pub use reserve::{Reserve, ReserveP}; +pub use with_label::WithLabel; diff --git a/crates/kas-widgets/src/adapter/label.rs b/crates/kas-widgets/src/adapter/with_label.rs similarity index 100% rename from crates/kas-widgets/src/adapter/label.rs rename to crates/kas-widgets/src/adapter/with_label.rs From 083726d27c5e1daa2511184b8cba47e999a06913 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 11 Jul 2022 11:09:59 +0100 Subject: [PATCH 02/27] examples/gallery: combine Label widgets --- examples/gallery.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gallery.rs b/examples/gallery.rs index 193e55e8f..4b70aab36 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -411,9 +411,9 @@ fn canvas() -> Box { Box::new(impl_singleton! { #[widget{ layout = column: [ - "Animated canvas demo", - "This example uses kas_resvg::Canvas, which is CPU-rendered.", - "Embedded GPU-rendered content is also possible (see separate Mandlebrot example).", + Label::new("Animated canvas demo +This example uses kas_resvg::Canvas, which is CPU-rendered. +Embedded GPU-rendered content is also possible (see separate Mandlebrot example)."), self.canvas, ]; }] From 93afce426f6e925063b265a0f0a08b3ae204eb28 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 11 Jul 2022 11:38:03 +0100 Subject: [PATCH 03/27] Add kas_core::label::StrLabel This avoids the need for macros to depend on kas_widgets Also: document macro syntax for implicit widgets in layout --- crates/kas-core/src/label.rs | 69 ++++++++++++++++++++++++++++ crates/kas-core/src/lib.rs | 3 ++ crates/kas-macros/src/lib.rs | 15 ++++-- crates/kas-macros/src/make_layout.rs | 4 +- 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 crates/kas-core/src/label.rs diff --git a/crates/kas-core/src/label.rs b/crates/kas-core/src/label.rs new file mode 100644 index 000000000..9cf9cbf98 --- /dev/null +++ b/crates/kas-core/src/label.rs @@ -0,0 +1,69 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Simplified text label +//! +//! This module supports text strings in macros. +//! Direct usage of this module from user code is not supported. + +use crate::class::HasStr; +use crate::event::ConfigMgr; +use crate::geom::Rect; +use crate::layout::{Align, AlignHints, AxisInfo, SizeRules}; +use crate::text::{Text, TextApi}; +use crate::theme::{DrawMgr, SizeMgr, TextClass}; +use crate::{Layout, WidgetCore}; +use kas_macros::impl_scope; + +impl_scope! { + /// A simple text label + /// + /// Vertical alignment defaults to centred, horizontal + /// alignment depends on the script direction if not specified. + /// Line-wrapping is enabled. + #[derive(Clone, Debug, Default)] + #[widget] + pub struct StrLabel { + core: widget_core!(), + label: Text<&'static str>, + } + + impl Self { + /// Construct from `label` + #[inline] + pub fn new(label: &'static str) -> Self { + StrLabel { + core: Default::default(), + label: Text::new(label), + } + } + + /// Text class + pub const CLASS: TextClass = TextClass::Label(true); + } + + impl Layout for Self { + #[inline] + fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + size_mgr.text_bound(&mut self.label, Self::CLASS, axis) + } + + fn set_rect(&mut self, mgr: &mut ConfigMgr, rect: Rect, align: AlignHints) { + self.core.rect = rect; + let align = align.unwrap_or(Align::Default, Align::Center); + mgr.text_set_size(&mut self.label, Self::CLASS, rect.size, align); + } + + fn draw(&mut self, mut draw: DrawMgr) { + draw.text(self.rect().pos, &self.label, Self::CLASS); + } + } + + impl HasStr for Self { + fn get_str(&self) -> &str { + self.label.as_str() + } + } +} diff --git a/crates/kas-core/src/lib.rs b/crates/kas-core/src/lib.rs index 2946c2dfb..f496b3819 100644 --- a/crates/kas-core/src/lib.rs +++ b/crates/kas-core/src/lib.rs @@ -42,6 +42,9 @@ pub mod dir; pub mod draw; pub mod event; pub mod geom; +#[cfg_attr(not(feature = "internal_doc"), doc(hidden))] +#[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] +pub mod label; pub mod layout; pub mod model; pub mod prelude; diff --git a/crates/kas-macros/src/lib.rs b/crates/kas-macros/src/lib.rs index 67cb2e56f..7f5983600 100644 --- a/crates/kas-macros/src/lib.rs +++ b/crates/kas-macros/src/lib.rs @@ -196,7 +196,7 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// The latter accepts the following syntax: /// /// > _Layout_ :\ -/// >       _Single_ | _List_ | _Slice_ | _Grid_ | _Float_ | _Align_ | _Frame_ | _Button_ +/// >       _Single_ | _List_ | _Slice_ | _Grid_ | _Float_ | _Align_ | _Frame_ | _Button_ | _LitStr_ | _ExprStartingUpperCaseIdent_ /// > /// > _Single_ :\ /// >    `self` `.` _Member_ | _Expr_ @@ -274,9 +274,18 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// type `Option`). Additionally, a button automatically uses centered /// alignment of content. /// +/// An identifier starting with an upper-case letter is interpreted as an +/// expression constructing a widget, which is inserted into the layout in the +/// given position. +/// +/// A string literal implicitly generates a string label widget. This widget +/// will wrap text when required. An alternative is `Label::new("abc...")` +/// (when `kas::widgets::Label` is in scope). +/// /// Non-trivial layouts require a "storage" field within the generated -/// `widget_core!()`. This storage field may be named via a "lifetime label" -/// (e.g. `col 'col_storage: [...]`), otherwise the field name will be generated. +/// `widget_core!()`. This storage field is usually anonymous (i.e. uses an +/// automatically generated name), but may be named via a "lifetime label", +/// for example: `col 'col_storage: [...]`. /// /// _Member_ is a field name (struct) or number (tuple struct). /// diff --git a/crates/kas-macros/src/make_layout.rs b/crates/kas-macros/src/make_layout.rs index 3349a8d50..731a79f5a 100644 --- a/crates/kas-macros/src/make_layout.rs +++ b/crates/kas-macros/src/make_layout.rs @@ -676,9 +676,9 @@ impl Layout { Layout::Label(stor, text) => { children.push(stor.to_token_stream()); stor.to_tokens(ty_toks); - ty_toks.append_all(quote! { : ::kas::widgets::StrLabel, }); + ty_toks.append_all(quote! { : ::kas::label::StrLabel, }); stor.to_tokens(def_toks); - def_toks.append_all(quote! { : ::kas::widgets::StrLabel::new(#text), }); + def_toks.append_all(quote! { : ::kas::label::StrLabel::new(#text), }); } } } From 9632f19ea91e5f4f54b63ad13b85a404017ce56e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 11:36:47 +0100 Subject: [PATCH 04/27] Replace Vec2::max_abs_comp with max_comp (can be combined with Vec2::abs) Also: use L-inf metric in EventState::config_test_pan_thresh --- crates/kas-core/src/event/components.rs | 2 +- crates/kas-core/src/event/manager/mgr_pub.rs | 3 +-- crates/kas-core/src/geom/vector.rs | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index b1f341ef3..050023ea7 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -80,7 +80,7 @@ impl Glide { let delta = Offset::conv_approx(d); let rest = d - Vec2::conv(delta); - if v.max_abs_comp() >= 1.0 { + if v.abs().max_comp() >= 1.0 { let mut v = *v * decay_mul.powf(dur); v = v - v.abs().min(Vec2::splat(decay_sub * dur)) * v.sign(); *self = Glide::Glide(now, v, rest); diff --git a/crates/kas-core/src/event/manager/mgr_pub.rs b/crates/kas-core/src/event/manager/mgr_pub.rs index b0431de4d..ec9e63f05 100644 --- a/crates/kas-core/src/event/manager/mgr_pub.rs +++ b/crates/kas-core/src/event/manager/mgr_pub.rs @@ -141,8 +141,7 @@ impl EventState { /// Returns true when `dist` is large enough to switch to pan mode. #[inline] pub fn config_test_pan_thresh(&self, dist: Offset) -> bool { - let thresh = self.config.pan_dist_thresh(); - Vec2::conv(dist).sum_square() >= thresh * thresh + Vec2::conv(dist).abs().max_comp() >= self.config.pan_dist_thresh() } /// Set/unset a widget as disabled diff --git a/crates/kas-core/src/geom/vector.rs b/crates/kas-core/src/geom/vector.rs index ac572ad21..dea412bbd 100644 --- a/crates/kas-core/src/geom/vector.rs +++ b/crates/kas-core/src/geom/vector.rs @@ -175,10 +175,10 @@ macro_rules! impl_vec2 { self.0.min(self.1) } - /// Take the maximum of absolute values of components + /// Take the maximum component #[inline] - pub fn max_abs_comp(self) -> $f { - self.0.abs().max(self.1.abs()) + pub fn max_comp(self) -> $f { + self.0.max(self.1) } /// Return the minimum, componentwise From 51b9b3bbf7b0fbce5abfd230ae96d9a8436c58b3 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 09:54:42 +0100 Subject: [PATCH 05/27] Add const TkAction::EMPTY --- crates/kas-core/src/toolkit.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/kas-core/src/toolkit.rs b/crates/kas-core/src/toolkit.rs index ca130eda3..a93dc229c 100644 --- a/crates/kas-core/src/toolkit.rs +++ b/crates/kas-core/src/toolkit.rs @@ -52,6 +52,10 @@ bitflags! { #[must_use] #[derive(Default)] pub struct TkAction: u32 { + /// No flags + /// + /// This is a [zero flag](https://docs.rs/bitflags/latest/bitflags/#zero-flags). + const EMPTY = 0; /// The whole window requires redrawing /// /// Note that [`event::EventMgr::redraw`] can instead be used for more From 1c259c54b65f91bd5d0953dd2805b5c3e08a3bed Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 11:09:24 +0100 Subject: [PATCH 06/27] Revise Spinner widget --- crates/kas-widgets/src/spinner.rs | 219 ++++++++++++++++++++---------- 1 file changed, 145 insertions(+), 74 deletions(-) diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index c73a99fce..1daeddde8 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -9,35 +9,68 @@ use crate::{EditField, EditGuard, MarkButton}; use kas::event::{Command, ScrollDelta}; use kas::prelude::*; use kas::theme::{Background, FrameStyle, MarkStyle}; -use std::ops::{Add, RangeInclusive, Sub}; +use std::cmp::Ord; +use std::ops::RangeInclusive; /// Requirements on type used by [`Spinner`] pub trait SpinnerType: - Copy - + Add - + Sub - + PartialOrd - + std::fmt::Debug - + std::str::FromStr - + ToString - + Sized - + 'static + Copy + PartialOrd + std::fmt::Debug + std::str::FromStr + ToString + 'static { + /// Clamp `self` to the range `l_bound..=u_bound` + fn clamp(self, l_bound: Self, u_bound: Self) -> Self; + + /// Add `x` without overflow, clamping the result to no more than `u_bound` + fn add_step(self, step: Self, u_bound: Self) -> Self; + + /// Subtract `step` without overflow, clamping the result to no less than `l_bound` + fn sub_step(self, step: Self, l_bound: Self) -> Self; } -impl< - T: Copy - + Add - + Sub - + PartialOrd - + std::fmt::Debug - + std::str::FromStr - + ToString - + Sized - + 'static, - > SpinnerType for T -{ + +macro_rules! impl_float { + ($t:ty) => { + impl SpinnerType for $t { + fn clamp(self, l_bound: Self, u_bound: Self) -> Self { + <$t>::clamp(self, l_bound, u_bound) + } + + fn add_step(self, step: Self, u_bound: Self) -> Self { + ((self / step + 1.0).round() * step).min(u_bound) + } + + fn sub_step(self, step: Self, l_bound: Self) -> Self { + ((self / step - 1.0).round() * step).max(l_bound) + } + } + }; } +impl_float!(f32); +impl_float!(f64); + +macro_rules! impl_int { + ($t:ty) => { + impl SpinnerType for $t { + fn clamp(self, l_bound: Self, u_bound: Self) -> Self { + Ord::clamp(self, l_bound, u_bound) + } + + fn add_step(self, step: Self, u_bound: Self) -> Self { + ((self / step).saturating_add(1)).saturating_mul(step).min(u_bound) + } + + fn sub_step(self, step: Self, l_bound: Self) -> Self { + (((self + step - 1) / step).saturating_sub(1)).saturating_mul(step).max(l_bound) + } + } + }; + ($($t:ty),*) => { + $(impl_int!($t);)* + }; +} + +impl_int!(i8, i16, i32, i64, i128, isize); +impl_int!(u8, u16, u32, u64, u128, usize); + #[derive(Clone, Debug)] enum SpinBtn { Down, @@ -45,36 +78,54 @@ enum SpinBtn { } #[derive(Clone, Debug)] -struct SpinnerGuard(T, RangeInclusive); +struct SpinnerGuard { + value: T, + start: T, + end: T, +} + impl SpinnerGuard { + fn new(range: RangeInclusive) -> Self { + SpinnerGuard { + value: *range.start(), + start: *range.start(), + end: *range.end(), + } + } + #[allow(clippy::neg_cmp_op_on_partial_ord)] fn set_value(&mut self, value: T) { - self.0 = if !(value >= *self.1.start()) { - *self.1.start() - } else if !(value <= *self.1.end()) { - *self.1.end() - } else { - value - }; + self.value = value.clamp(self.start, self.end); + } + + fn range(&self) -> RangeInclusive { + self.start..=self.end } } + impl EditGuard for SpinnerGuard { fn activate(edit: &mut EditField, mgr: &mut EventMgr) { if edit.has_error() { - *mgr |= edit.set_string(edit.guard.0.to_string()); + *mgr |= edit.set_string(edit.guard.value.to_string()); edit.set_error_state(false); } - mgr.push_msg(edit.guard.0); + mgr.push_msg(edit.guard.value); } fn focus_lost(edit: &mut EditField, mgr: &mut EventMgr) { - Self::activate(edit, mgr); + if edit.has_error() { + *mgr |= edit.set_string(edit.guard.value.to_string()); + edit.set_error_state(false); + } } - fn edit(edit: &mut EditField, _: &mut EventMgr) { + fn edit(edit: &mut EditField, mgr: &mut EventMgr) { let is_err = match edit.get_str().parse() { - Ok(value) if edit.guard.1.contains(&value) => { - edit.guard.0 = value; + Ok(value) if edit.guard.range().contains(&value) => { + if value != edit.guard.value { + edit.guard.value = value; + mgr.push_msg(value); + } false } Ok(value) => { @@ -90,7 +141,21 @@ impl EditGuard for SpinnerGuard { impl_scope! { /// A numeric entry widget with up/down arrows /// - /// Sends a message of type `T` on edit. + /// The value is constrained to a given `range`. Increment and decrement + /// operations advance to the next/previous multiple of `step`. + /// + /// Recommendations for optimal behaviour: + /// + /// - Ensure that range end points are a multiple of `step` + /// - With floating-point types, ensure that `step` is exactly + /// representable, e.g. an integer or a power of 2. + /// + /// Sends a message of type `T` when changed, specifically: + /// + /// - If the increment/decrement buttons, Up/Down + /// keys or mouse scroll wheel is used and the value changes + /// - If the value is adjusted via the edit box and the result is valid + /// - If Enter is pressed in the edit box #[derive(Clone, Debug)] #[widget { layout = frame(FrameStyle::EditBox): row: [ @@ -113,19 +178,14 @@ impl_scope! { } impl Self { - /// Construct + /// Construct with given `range` and `step` pub fn new(range: RangeInclusive, step: T) -> Self { - assert!(!range.is_empty()); - let min = *range.start(); - let mut guard = SpinnerGuard(min, range); - guard.set_value(min); - Spinner { core: Default::default(), - edit: EditField::new(guard.0.to_string()).with_guard(guard), + edit: EditField::new("").with_guard(SpinnerGuard::new(range)), b_up: MarkButton::new(MarkStyle::Point(Direction::Up), SpinBtn::Up), b_down: MarkButton::new(MarkStyle::Point(Direction::Down), SpinBtn::Down), - step, + step: step, } } @@ -140,26 +200,36 @@ impl_scope! { /// Get the current value #[inline] pub fn value(&self) -> T { - self.edit.guard.0 + self.edit.guard.value } /// Set the value /// /// Returns [`TkAction::REDRAW`] if a redraw is required. pub fn set_value(&mut self, value: T) -> TkAction { - if self.edit.guard.0 == value { - return TkAction::empty(); - } - - self.edit.guard.set_value(value); self.edit.set_error_state(false); - self.edit.set_string(self.edit.guard.0.to_string()) + let old_value = self.edit.guard.value; + self.edit.guard.set_value(value); + if self.edit.guard.value != old_value { + self.edit.set_string(self.edit.guard.value.to_string()) + } else { + TkAction::empty() + } } - fn set_and_emit(&mut self, mgr: &mut EventMgr, value: T) -> Response { - *mgr |= self.set_value(value); - mgr.push_msg(self.value()); - Response::Used + fn handle_btn(&mut self, mgr: &mut EventMgr, btn: SpinBtn) { + let value = match btn { + SpinBtn::Down => self.value().sub_step(self.step, self.edit.guard.start), + SpinBtn::Up => self.value().add_step(self.step, self.edit.guard.end), + }; + + if value != self.edit.guard.value { + self.edit.guard.value = value; + mgr.push_msg(value); + } + + self.edit.set_error_state(false); + *mgr |= self.edit.set_string(value.to_string()); } } @@ -178,37 +248,38 @@ impl_scope! { } impl Widget for Self { + fn configure(&mut self, mgr: &mut ConfigMgr) { + *mgr |= self.edit.set_string(self.edit.guard.value.to_string()); + } + fn steal_event(&mut self, mgr: &mut EventMgr, _: &WidgetId, event: &Event) -> Response { - match event { + let btn = match event { Event::Command(cmd) => { - let value = match cmd { - Command::Down => self.value() - self.step, - Command::Up => self.value() + self.step, + match cmd { + Command::Down => SpinBtn::Down, + Command::Up => SpinBtn::Up, _ => return Response::Unused, - }; - self.set_and_emit(mgr, value) + } } Event::Scroll(ScrollDelta::LineDelta(_, y)) => { if *y > 0.0 { - self.set_and_emit(mgr, self.value() + self.step) + SpinBtn::Up } else if *y < 0.0 { - self.set_and_emit(mgr, self.value() - self.step) + SpinBtn::Down } else { - Response::Unused + return Response::Unused; } } - _ => Response::Unused, - } + _ => return Response::Unused, + }; + + self.handle_btn(mgr, btn); + Response::Used } fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - if let Some(btn) = mgr.try_pop_msg() { - let value = match btn { - SpinBtn::Down => self.value() - self.step, - SpinBtn::Up => self.value() + self.step, - }; - *mgr |= self.set_value(value); - mgr.push_msg(self.value()); + if let Some(btn) = mgr.try_pop_msg::() { + self.handle_btn(mgr, btn); } } } From 7174275253a26f6fa0cbe79f3a46ffcf98cc35be Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 12:05:27 +0100 Subject: [PATCH 07/27] Rename Spinner/SliderType traits to Spinner/SliderValue --- crates/kas-widgets/src/lib.rs | 4 ++-- crates/kas-widgets/src/slider.rs | 14 ++++++++------ crates/kas-widgets/src/spinner.rs | 16 +++++++++------- crates/kas-widgets/src/view/driver.rs | 18 +++++++++--------- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/kas-widgets/src/lib.rs b/crates/kas-widgets/src/lib.rs index fd02f369e..ee5dc2b37 100644 --- a/crates/kas-widgets/src/lib.rs +++ b/crates/kas-widgets/src/lib.rs @@ -107,8 +107,8 @@ pub use scroll::ScrollRegion; pub use scroll_bar::{ScrollBar, ScrollBarRegion, ScrollBars}; pub use scroll_label::ScrollLabel; pub use separator::Separator; -pub use slider::{Slider, SliderType}; -pub use spinner::{Spinner, SpinnerType}; +pub use slider::{Slider, SliderValue}; +pub use spinner::{Spinner, SpinnerValue}; pub use splitter::*; pub use stack::{BoxStack, RefStack, Stack}; pub use tab_stack::{BoxTabStack, Tab, TabStack}; diff --git a/crates/kas-widgets/src/slider.rs b/crates/kas-widgets/src/slider.rs index f7d6b6471..aab2f9c32 100644 --- a/crates/kas-widgets/src/slider.rs +++ b/crates/kas-widgets/src/slider.rs @@ -15,7 +15,9 @@ use kas::prelude::*; use kas::theme::Feature; /// Requirements on type used by [`Slider`] -pub trait SliderType: +/// +/// Implementations are provided for standard float and integer types. +pub trait SliderValue: Copy + Debug + PartialOrd + Add + Sub + 'static { /// Divide self by another instance of this type, returning an `f64` @@ -35,7 +37,7 @@ pub trait SliderType: fn mul_f64(self, scalar: f64) -> Self; } -impl SliderType for f64 { +impl SliderValue for f64 { fn div_as_f64(self, rhs: Self) -> f64 { self / rhs } @@ -44,7 +46,7 @@ impl SliderType for f64 { } } -impl SliderType for f32 { +impl SliderValue for f32 { fn div_as_f64(self, rhs: Self) -> f64 { self as f64 / rhs as f64 } @@ -55,7 +57,7 @@ impl SliderType for f32 { macro_rules! impl_slider_ty { ($ty:ty) => { - impl SliderType for $ty { + impl SliderValue for $ty { fn div_as_f64(self, rhs: Self) -> f64 { self as f64 / rhs as f64 } @@ -74,7 +76,7 @@ macro_rules! impl_slider_ty { impl_slider_ty!(i8, i16, i32, i64, i128, isize); impl_slider_ty!(u8, u16, u32, u64, u128, usize); -impl SliderType for Duration { +impl SliderValue for Duration { fn div_as_f64(self, rhs: Self) -> f64 { self.as_secs_f64() / rhs.as_secs_f64() } @@ -96,7 +98,7 @@ impl_scope! { key_nav = true; hover_highlight = true; }] - pub struct Slider { + pub struct Slider { core: widget_core!(), direction: D, // Terminology assumes vertical orientation: diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index 1daeddde8..c2d726597 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -13,7 +13,9 @@ use std::cmp::Ord; use std::ops::RangeInclusive; /// Requirements on type used by [`Spinner`] -pub trait SpinnerType: +/// +/// Implementations are provided for standard float and integer types. +pub trait SpinnerValue: Copy + PartialOrd + std::fmt::Debug + std::str::FromStr + ToString + 'static { /// Clamp `self` to the range `l_bound..=u_bound` @@ -28,7 +30,7 @@ pub trait SpinnerType: macro_rules! impl_float { ($t:ty) => { - impl SpinnerType for $t { + impl SpinnerValue for $t { fn clamp(self, l_bound: Self, u_bound: Self) -> Self { <$t>::clamp(self, l_bound, u_bound) } @@ -49,7 +51,7 @@ impl_float!(f64); macro_rules! impl_int { ($t:ty) => { - impl SpinnerType for $t { + impl SpinnerValue for $t { fn clamp(self, l_bound: Self, u_bound: Self) -> Self { Ord::clamp(self, l_bound, u_bound) } @@ -78,13 +80,13 @@ enum SpinBtn { } #[derive(Clone, Debug)] -struct SpinnerGuard { +struct SpinnerGuard { value: T, start: T, end: T, } -impl SpinnerGuard { +impl SpinnerGuard { fn new(range: RangeInclusive) -> Self { SpinnerGuard { value: *range.start(), @@ -103,7 +105,7 @@ impl SpinnerGuard { } } -impl EditGuard for SpinnerGuard { +impl EditGuard for SpinnerGuard { fn activate(edit: &mut EditField, mgr: &mut EventMgr) { if edit.has_error() { *mgr |= edit.set_string(edit.guard.value.to_string()); @@ -166,7 +168,7 @@ impl_scope! { ], ]; }] - pub struct Spinner { + pub struct Spinner { core: widget_core!(), #[widget] edit: EditField>, diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index 68ef91ed0..390ba5098 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -9,8 +9,8 @@ //! allowing referal to e.g. `driver::DefaultView`. use crate::{ - CheckBox, EditBox, EditField, EditGuard, Label, NavFrame, ProgressBar, RadioGroup, SliderType, - SpinnerType, + CheckBox, EditBox, EditField, EditGuard, Label, NavFrame, ProgressBar, RadioGroup, SliderValue, + SpinnerValue, }; use kas::prelude::*; use std::default::Default; @@ -247,12 +247,12 @@ impl Driver for RadioButton { /// [`crate::Slider`] view widget constructor #[derive(Clone, Debug)] -pub struct Slider { +pub struct Slider { range: RangeInclusive, step: T, direction: D, } -impl Slider { +impl Slider { /// Construct, with given `range` and `step` (see [`crate::Slider::new`]) pub fn make(range: RangeInclusive, step: T) -> Self { Slider { @@ -262,7 +262,7 @@ impl Slider { } } } -impl Slider { +impl Slider { /// Construct, with given `range`, `step` and `direction` (see [`Slider::new_with_direction`]) pub fn new_with_direction(range: RangeInclusive, step: T, direction: D) -> Self { Slider { @@ -272,7 +272,7 @@ impl Slider { } } } -impl Driver for Slider { +impl Driver for Slider { type Widget = crate::Slider; fn make(&self) -> Self::Widget { crate::Slider::new_with_direction(self.range.clone(), self.step, self.direction) @@ -284,17 +284,17 @@ impl Driver for Slider { /// [`crate::Spinner`] view widget constructor #[derive(Clone, Debug)] -pub struct Spinner { +pub struct Spinner { range: RangeInclusive, step: T, } -impl Spinner { +impl Spinner { /// Construct, with given `range` and `step` (see [`crate::Spinner::new`]) pub fn make(range: RangeInclusive, step: T) -> Self { Spinner { range, step } } } -impl Driver for Spinner { +impl Driver for Spinner { type Widget = crate::Spinner; fn make(&self) -> Self::Widget { crate::Spinner::new(self.range.clone(), self.step) From 0641869be5e70243a3540e11b848ec3bcce67d8a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 11 Jul 2022 10:35:29 +0100 Subject: [PATCH 08/27] Add impl of Driver --- crates/kas-widgets/src/view/driver.rs | 2 + crates/kas-widgets/src/view/driver/config.rs | 90 ++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 crates/kas-widgets/src/view/driver/config.rs diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index 390ba5098..14de12951 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -8,6 +8,8 @@ //! Intended usage is to import the module name rather than its contents, thus //! allowing referal to e.g. `driver::DefaultView`. +mod config; + use crate::{ CheckBox, EditBox, EditField, EditGuard, Label, NavFrame, ProgressBar, RadioGroup, SliderValue, SpinnerValue, diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs new file mode 100644 index 000000000..9b7b58844 --- /dev/null +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -0,0 +1,90 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Drivers for configuration types + +use crate::view::driver; +use crate::{CheckButton, ComboBox, Spinner}; +use kas::event::config::MousePan; +use kas::prelude::*; + +impl_scope! { + /// A widget for viewing event config + #[widget{ + layout = grid: { + 0, 0: "Menu delay:"; 1, 0: self.menu_delay; 2, 0: "ms"; + 0, 1: "Touch-selection delay:"; 1, 1: self.touch_select_delay; 2, 1: "ms"; + 0, 2: "Scroll flick timeout:"; 1, 2: self.scroll_flick_timeout; 2, 2: "ms"; + 0, 3: "Scroll flick multiply:"; 1, 3: self.scroll_flick_mul; + 0, 4: "Scroll flick subtract:"; 1, 4: self.scroll_flick_sub; + 0, 5: "Pan distance threshold:"; 1, 5: self.pan_dist_thresh; + 0, 6: "Mouse pan:"; 1..3, 6: self.mouse_pan; + 0, 7: "Mouse text pan:"; 1..3, 7: self.mouse_text_pan; + 1..3, 8: self.mouse_nav_focus; + 1..3, 9: self.touch_nav_focus; + }; + }] + #[derive(Debug)] + pub struct EventConfig { + core: widget_core!(), + #[widget] menu_delay: Spinner, + #[widget] touch_select_delay: Spinner, + #[widget] scroll_flick_timeout: Spinner, + #[widget] scroll_flick_mul: Spinner, + #[widget] scroll_flick_sub: Spinner, + #[widget] pan_dist_thresh: Spinner, + #[widget] mouse_pan: ComboBox, + #[widget] mouse_text_pan: ComboBox, + #[widget] mouse_nav_focus: CheckButton, + #[widget] touch_nav_focus: CheckButton, + } +} + +impl driver::Driver for driver::DefaultView { + type Widget = EventConfig; + + fn make(&self) -> Self::Widget { + let mouse_pan = ComboBox::from([ + ("Never", MousePan::Never), + ("With Alt key", MousePan::WithAlt), + ("With Ctrl key", MousePan::WithCtrl), + ("Always", MousePan::Always), + ]); + let mouse_text_pan = mouse_pan.clone(); + + EventConfig { + core: Default::default(), + menu_delay: Spinner::new(0..=10_000, 50), + touch_select_delay: Spinner::new(0..=10_000, 50), + scroll_flick_timeout: Spinner::new(0..=1_000, 5), + scroll_flick_mul: Spinner::new(0.0..=1.0, 0.0625), + scroll_flick_sub: Spinner::new(0.0..=1.0e4, 10.0), + pan_dist_thresh: Spinner::new(0.125..=10.0, 0.125), + mouse_pan, + mouse_text_pan, + mouse_nav_focus: CheckButton::new("Mouse navigation focus"), + touch_nav_focus: CheckButton::new("Touchscreen navigation focus"), + } + } + + fn set(&self, widget: &mut Self::Widget, data: kas::event::Config) -> TkAction { + widget.menu_delay.set_value(data.menu_delay_ms) + | widget + .touch_select_delay + .set_value(data.touch_select_delay_ms) + | widget + .scroll_flick_timeout + .set_value(data.scroll_flick_timeout_ms) + | widget.scroll_flick_mul.set_value(data.scroll_flick_mul) + | widget.scroll_flick_sub.set_value(data.scroll_flick_sub) + | widget.pan_dist_thresh.set_value(data.pan_dist_thresh) + | widget.mouse_pan.set_active(data.mouse_pan as usize) + | widget + .mouse_text_pan + .set_active(data.mouse_text_pan as usize) + | widget.mouse_nav_focus.set_bool(data.mouse_nav_focus) + | widget.touch_nav_focus.set_bool(data.touch_nav_focus) + } +} From 9bd3ad455e6504744b36a6621e57a42b34cd5acf Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 12:43:13 +0100 Subject: [PATCH 09/27] Use SharedRc instead of Rc> --- crates/kas-core/src/event/config.rs | 7 ++- crates/kas-core/src/event/manager.rs | 2 - .../kas-core/src/event/manager/mgr_shell.rs | 3 +- crates/kas-core/src/model/data_traits.rs | 4 +- crates/kas-core/src/model/mod.rs | 2 +- crates/kas-core/src/model/shared_rc.rs | 44 ++++++++++++++++++- crates/kas-wgpu/src/lib.rs | 10 ++--- crates/kas-wgpu/src/shared.rs | 7 ++- 8 files changed, 58 insertions(+), 21 deletions(-) diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/event/config.rs index e46836567..1b0204e28 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/event/config.rs @@ -11,10 +11,9 @@ pub use shortcuts::Shortcuts; use super::ModifiersState; use crate::cast::{Cast, CastFloat}; use crate::geom::Offset; +use crate::model::SharedRc; #[cfg(feature = "config")] use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::rc::Rc; use std::time::Duration; /// Event handling configuration @@ -97,7 +96,7 @@ impl Default for Config { /// Wrapper around [`Config`] to handle window-specific scaling #[derive(Clone, Debug)] pub struct WindowConfig { - config: Rc>, + config: SharedRc, scroll_dist: f32, scroll_flick_sub: f32, pan_dist_thresh: f32, @@ -107,7 +106,7 @@ impl WindowConfig { /// Construct #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn new(config: Rc>, scale_factor: f32) -> Self { + pub fn new(config: SharedRc, scale_factor: f32) -> Self { let mut w = WindowConfig { config, scroll_dist: f32::NAN, diff --git a/crates/kas-core/src/event/manager.rs b/crates/kas-core/src/event/manager.rs index 7f356f089..aa601d514 100644 --- a/crates/kas-core/src/event/manager.rs +++ b/crates/kas-core/src/event/manager.rs @@ -12,10 +12,8 @@ use linear_map::LinearMap; use log::{trace, warn}; use smallvec::SmallVec; use std::any::{type_name, Any}; -use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, VecDeque}; use std::ops::{Deref, DerefMut}; -use std::rc::Rc; use std::time::Instant; use std::u16; diff --git a/crates/kas-core/src/event/manager/mgr_shell.rs b/crates/kas-core/src/event/manager/mgr_shell.rs index 3ff3b0694..aa059c622 100644 --- a/crates/kas-core/src/event/manager/mgr_shell.rs +++ b/crates/kas-core/src/event/manager/mgr_shell.rs @@ -12,6 +12,7 @@ use std::time::{Duration, Instant}; use super::*; use crate::cast::traits::*; use crate::geom::{Coord, DVec2}; +use crate::model::SharedRc; use crate::{ShellWindow, TkAction, Widget, WidgetId}; // TODO: this should be configurable or derived from the system @@ -25,7 +26,7 @@ const FAKE_MOUSE_BUTTON: MouseButton = MouseButton::Other(0); impl EventState { /// Construct an event manager per-window data struct #[inline] - pub fn new(config: Rc>, scale_factor: f32) -> Self { + pub fn new(config: SharedRc, scale_factor: f32) -> Self { EventState { config: WindowConfig::new(config, scale_factor), disabled: vec![], diff --git a/crates/kas-core/src/model/data_traits.rs b/crates/kas-core/src/model/data_traits.rs index b49615196..266d46de7 100644 --- a/crates/kas-core/src/model/data_traits.rs +++ b/crates/kas-core/src/model/data_traits.rs @@ -31,7 +31,7 @@ pub trait SingleData: Debug { /// update. fn version(&self) -> u64; - // TODO(gat): add get<'a>(&self) -> Self::ItemRef<'a> and get_mut + // TODO(gat): add borrow<'a>(&self) -> Self::ItemRef<'a>, try_borrow? /// Get data (clone) fn get_cloned(&self) -> Self::Item; @@ -62,6 +62,8 @@ pub trait SingleData: Debug { /// Trait for writable single data items #[autoimpl(for &mut T, Box)] pub trait SingleDataMut: SingleData { + // TODO(gat): add borrow_mut<'a>(&self) -> Self::ItemMutRef<'a>, try_borrow_mut? + /// Set data, given a mutable (unique) reference /// /// It can be assumed that no synchronisation is required when a mutable diff --git a/crates/kas-core/src/model/mod.rs b/crates/kas-core/src/model/mod.rs index d759add6f..bbb12f74b 100644 --- a/crates/kas-core/src/model/mod.rs +++ b/crates/kas-core/src/model/mod.rs @@ -16,4 +16,4 @@ mod shared_rc; pub use data_traits::{ ListData, ListDataMut, MatrixData, MatrixDataMut, SingleData, SingleDataMut, }; -pub use shared_rc::SharedRc; +pub use shared_rc::{SharedRc, SharedRcRef}; diff --git a/crates/kas-core/src/model/shared_rc.rs b/crates/kas-core/src/model/shared_rc.rs index d13127f9d..9c8e2354d 100644 --- a/crates/kas-core/src/model/shared_rc.rs +++ b/crates/kas-core/src/model/shared_rc.rs @@ -14,16 +14,31 @@ use crate::event::EventMgr; use crate::event::UpdateId; use crate::model::*; use crate::WidgetId; -use std::cell::RefCell; +use std::cell::{BorrowError, Ref, RefCell}; use std::fmt::Debug; +use std::ops::Deref; use std::rc::Rc; /// Wrapper for single-thread shared data /// -/// This wrapper adds an [`UpdateId`]. +/// This is vaguely `Rc>`, but includes an [`UpdateId`] and a `u64` +/// version counter. +/// +/// The wrapped value may be read via [`Self::borrow`], [`Self::try_borrow`] and +/// [`SingleData::get_cloned`]. +/// +/// The value may be set via [`SingleData::update`] and [`SingleDataMut::set`]. #[derive(Clone, Debug, Default)] pub struct SharedRc(Rc<(UpdateId, RefCell<(T, u64)>)>); +pub struct SharedRcRef<'a, T>(Ref<'a, (T, u64)>); +impl<'a, T> Deref for SharedRcRef<'a, T> { + type Target = T; + fn deref(&self) -> &T { + &self.0.deref().0 + } +} + impl SharedRc { /// Construct with given data pub fn new(data: T) -> Self { @@ -38,6 +53,31 @@ impl SharedRc { pub fn id(&self) -> UpdateId { (self.0).0 } + + /// Immutably borrows the wrapped value, returning an error if the value is currently mutably + /// borrowed. + /// + /// The borrow lasts until the returned `Ref` exits scope. Multiple immutable borrows can be + /// taken out at the same time. + /// + /// # Panics + /// + /// Panics if the value is currently mutably borrowed. + /// For a non-panicking variant, use [`Self::try_borrow`]. + pub fn borrow(&self) -> SharedRcRef { + SharedRcRef((self.0).1.borrow()) + } + + /// Immutably borrows the wrapped value, returning an error if the value is currently mutably + /// borrowed. + /// + /// The borrow lasts until the returned `Ref` exits scope. Multiple immutable borrows can be + /// taken out at the same time. + /// + /// This is the non-panicking variant of [`Self::borrow`]. + pub fn try_borrow(&self) -> Result, BorrowError> { + (self.0).1.try_borrow().map(SharedRcRef) + } } impl SingleData for SharedRc { diff --git a/crates/kas-wgpu/src/lib.rs b/crates/kas-wgpu/src/lib.rs index 5f3e3faf7..9df24bd86 100644 --- a/crates/kas-wgpu/src/lib.rs +++ b/crates/kas-wgpu/src/lib.rs @@ -31,14 +31,12 @@ pub mod options; mod shared; mod window; -use std::cell::RefCell; -use std::rc::Rc; -use thiserror::Error; - use kas::draw::DrawShared; use kas::event::UpdateId; +use kas::model::SharedRc; use kas::WindowId; use kas_theme::Theme; +use thiserror::Error; use winit::error::OsError; use winit::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; @@ -153,7 +151,7 @@ where Default::default() } }; - let config = Rc::new(RefCell::new(config)); + let config = SharedRc::new(config); let scale_factor = find_scale_factor(&el); Ok(Toolkit { el, @@ -174,7 +172,7 @@ where custom: CB, theme: T, options: Options, - config: Rc>, + config: SharedRc, ) -> Result { let el = EventLoop::with_user_event(); let scale_factor = find_scale_factor(&el); diff --git a/crates/kas-wgpu/src/shared.rs b/crates/kas-wgpu/src/shared.rs index 99ad17b6d..ecf3208fc 100644 --- a/crates/kas-wgpu/src/shared.rs +++ b/crates/kas-wgpu/src/shared.rs @@ -6,9 +6,7 @@ //! Shared state use log::info; -use std::cell::RefCell; use std::num::NonZeroU32; -use std::rc::Rc; use std::time::Duration; use crate::draw::{CustomPipe, CustomPipeBuilder, DrawPipe, DrawWindow}; @@ -16,6 +14,7 @@ use crate::{warn_about_error, Error, Options, WindowId}; use kas::cast::Conv; use kas::draw; use kas::event::UpdateId; +use kas::model::SharedRc; use kas::TkAction; use kas_theme::{Theme, ThemeConfig}; @@ -29,7 +28,7 @@ pub struct SharedState { pub instance: wgpu::Instance, pub draw: draw::SharedState>, pub theme: T, - pub config: Rc>, + pub config: SharedRc, pub pending: Vec, /// Estimated scale factor (from last window constructed or available screens) pub scale_factor: f64, @@ -47,7 +46,7 @@ where custom: CB, mut theme: T, options: Options, - config: Rc>, + config: SharedRc, scale_factor: f64, ) -> Result { let instance = wgpu::Instance::new(options.backend()); From dfeadd7694dcc391b5d3d7455f6e8c4864bf2d86 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 12:59:28 +0100 Subject: [PATCH 10/27] Manager: update cached event::Config on update event --- crates/kas-core/src/event/config.rs | 27 ++++++++++--------- crates/kas-core/src/event/manager.rs | 1 + .../kas-core/src/event/manager/mgr_shell.rs | 8 +++++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/event/config.rs index 1b0204e28..93a05b5d0 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/event/config.rs @@ -93,10 +93,18 @@ impl Default for Config { } } +impl Config { + /// Has the config ever been updated? + #[inline] + pub fn is_dirty(&self) -> bool { + false // current code never updates config + } +} + /// Wrapper around [`Config`] to handle window-specific scaling #[derive(Clone, Debug)] pub struct WindowConfig { - config: SharedRc, + pub(crate) config: SharedRc, scroll_dist: f32, scroll_flick_sub: f32, pan_dist_thresh: f32, @@ -113,21 +121,23 @@ impl WindowConfig { scroll_flick_sub: f32::NAN, pan_dist_thresh: f32::NAN, }; - w.set_scale_factor(scale_factor); + w.update(scale_factor); w } - /// Set scale factor + /// Update #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn set_scale_factor(&mut self, scale_factor: f32) { + pub fn update(&mut self, scale_factor: f32) { let base = self.config.borrow(); const LINE_HEIGHT: f32 = 19.0; // TODO: maybe we shouldn't assume this? self.scroll_dist = base.scroll_lines * LINE_HEIGHT; self.scroll_flick_sub = base.scroll_flick_sub * scale_factor; self.pan_dist_thresh = base.pan_dist_thresh * scale_factor; } +} +impl WindowConfig { /// Delay before opening/closing menus on mouse hover #[inline] pub fn menu_delay(&self) -> Duration { @@ -225,15 +235,6 @@ impl WindowConfig { } } -/// Other functions -impl Config { - /// Has the config ever been updated? - #[inline] - pub fn is_dirty(&self) -> bool { - false // current code never updates config - } -} - /// When mouse-panning is enabled (click+drag to scroll) /// /// For *text* objects, this may conflict with text selection, hence it is diff --git a/crates/kas-core/src/event/manager.rs b/crates/kas-core/src/event/manager.rs index aa601d514..7c6535c4d 100644 --- a/crates/kas-core/src/event/manager.rs +++ b/crates/kas-core/src/event/manager.rs @@ -148,6 +148,7 @@ type AccelLayer = (bool, HashMap); #[derive(Debug)] pub struct EventState { config: WindowConfig, + scale_factor: f32, disabled: Vec, window_has_focus: bool, modifiers: ModifiersState, diff --git a/crates/kas-core/src/event/manager/mgr_shell.rs b/crates/kas-core/src/event/manager/mgr_shell.rs index aa059c622..3d907aad5 100644 --- a/crates/kas-core/src/event/manager/mgr_shell.rs +++ b/crates/kas-core/src/event/manager/mgr_shell.rs @@ -29,6 +29,7 @@ impl EventState { pub fn new(config: SharedRc, scale_factor: f32) -> Self { EventState { config: WindowConfig::new(config, scale_factor), + scale_factor, disabled: vec![], window_has_focus: false, modifiers: ModifiersState::empty(), @@ -57,7 +58,8 @@ impl EventState { /// Update scale factor pub fn set_scale_factor(&mut self, scale_factor: f32) { - self.config.set_scale_factor(scale_factor); + self.config.update(scale_factor); + self.scale_factor = scale_factor; } /// Configure event manager for a widget tree. @@ -252,6 +254,10 @@ impl<'a> EventMgr<'a> { /// Update widgets with an [`UpdateId`] pub fn update_widgets(&mut self, widget: &mut dyn Widget, id: UpdateId, payload: u64) { + if id == self.state.config.config.id() { + self.state.config.update(self.scale_factor); + } + let start = Instant::now(); let count = self.send_all(widget, Event::Update { id, payload }); debug!( From f2605c40de43ba186f05591f7f1e7855766df142 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 15:06:10 +0100 Subject: [PATCH 11/27] SharedRc is not for List/Matrix data --- crates/kas-core/src/model/shared_rc.rs | 116 +------------------------ 1 file changed, 3 insertions(+), 113 deletions(-) diff --git a/crates/kas-core/src/model/shared_rc.rs b/crates/kas-core/src/model/shared_rc.rs index 9c8e2354d..0ce94e975 100644 --- a/crates/kas-core/src/model/shared_rc.rs +++ b/crates/kas-core/src/model/shared_rc.rs @@ -13,7 +13,6 @@ use crate::event::EventMgr; use crate::event::UpdateId; use crate::model::*; -use crate::WidgetId; use std::cell::{BorrowError, Ref, RefCell}; use std::fmt::Debug; use std::ops::Deref; @@ -28,6 +27,9 @@ use std::rc::Rc; /// [`SingleData::get_cloned`]. /// /// The value may be set via [`SingleData::update`] and [`SingleDataMut::set`]. +/// +/// This wrapper type may be useful for simple shared data, but for more complex +/// uses a custom wrapper type may be required. #[derive(Clone, Debug, Default)] pub struct SharedRc(Rc<(UpdateId, RefCell<(T, u64)>)>); @@ -103,115 +105,3 @@ impl SingleDataMut for SharedRc { (self.0).1.borrow_mut().0 = value; } } - -impl ListData for SharedRc { - type Key = T::Key; - type Item = T::Item; - - fn version(&self) -> u64 { - let cell = (self.0).1.borrow(); - cell.0.version() + cell.1 - } - - fn len(&self) -> usize { - (self.0).1.borrow().0.len() - } - - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - (self.0).1.borrow().0.make_id(parent, key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - (self.0).1.borrow().0.reconstruct_key(parent, child) - } - - fn contains_key(&self, key: &Self::Key) -> bool { - (self.0).1.borrow().0.contains_key(key) - } - - fn get_cloned(&self, key: &Self::Key) -> Option { - (self.0).1.borrow().0.get_cloned(key) - } - - fn update(&self, mgr: &mut EventMgr, key: &Self::Key, value: Self::Item) { - let mut cell = (self.0).1.borrow_mut(); - cell.0.set(key, value); - cell.1 += 1; - mgr.update_all((self.0).0, 0); - } - - fn iter_vec(&self, limit: usize) -> Vec { - (self.0).1.borrow().0.iter_vec(limit) - } - - fn iter_vec_from(&self, start: usize, limit: usize) -> Vec { - (self.0).1.borrow().0.iter_vec_from(start, limit) - } -} -impl ListDataMut for SharedRc { - fn set(&mut self, key: &Self::Key, item: Self::Item) { - (self.0).1.borrow_mut().0.set(key, item); - } -} - -impl MatrixData for SharedRc { - type ColKey = T::ColKey; - type RowKey = T::RowKey; - type Key = T::Key; - type Item = T::Item; - - fn version(&self) -> u64 { - let cell = (self.0).1.borrow(); - cell.0.version() + cell.1 - } - - fn is_empty(&self) -> bool { - (self.0).1.borrow().0.is_empty() - } - fn len(&self) -> (usize, usize) { - (self.0).1.borrow().0.len() - } - - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - (self.0).1.borrow().0.make_id(parent, key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - (self.0).1.borrow().0.reconstruct_key(parent, child) - } - - fn contains(&self, key: &Self::Key) -> bool { - (self.0).1.borrow().0.contains(key) - } - fn get_cloned(&self, key: &Self::Key) -> Option { - (self.0).1.borrow().0.get_cloned(key) - } - - fn update(&self, mgr: &mut EventMgr, key: &Self::Key, value: Self::Item) { - let mut cell = (self.0).1.borrow_mut(); - cell.0.set(key, value); - cell.1 += 1; - mgr.update_all((self.0).0, 0); - } - - fn col_iter_vec(&self, limit: usize) -> Vec { - (self.0).1.borrow().0.col_iter_vec(limit) - } - fn col_iter_vec_from(&self, start: usize, limit: usize) -> Vec { - (self.0).1.borrow().0.col_iter_vec_from(start, limit) - } - - fn row_iter_vec(&self, limit: usize) -> Vec { - (self.0).1.borrow().0.row_iter_vec(limit) - } - fn row_iter_vec_from(&self, start: usize, limit: usize) -> Vec { - (self.0).1.borrow().0.row_iter_vec_from(start, limit) - } - - fn make_key(col: &Self::ColKey, row: &Self::RowKey) -> Self::Key { - T::make_key(col, row) - } -} -impl MatrixDataMut for SharedRc { - fn set(&mut self, key: &Self::Key, item: Self::Item) { - (self.0).1.borrow_mut().0.set(key, item); - } -} From 69f68dac3728bf5d45f97491cc09fa059ef08e14 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 16:45:20 +0100 Subject: [PATCH 12/27] =?UTF-8?q?kas::model:=20rename=20SingleData=20?= =?UTF-8?q?=E2=86=92=20SharedData=20and=20use=20as=20base=20of=20List/Matr?= =?UTF-8?q?ixData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/kas-core/src/model/data_impls.rs | 39 +++--- crates/kas-core/src/model/data_traits.rs | 155 ++++----------------- crates/kas-core/src/model/filter.rs | 34 +++-- crates/kas-core/src/model/mod.rs | 4 +- crates/kas-core/src/model/shared_rc.rs | 26 ++-- crates/kas-widgets/src/dialog.rs | 8 +- crates/kas-widgets/src/radio_box.rs | 8 +- crates/kas-widgets/src/view/filter_list.rs | 33 +++-- crates/kas-widgets/src/view/list_view.rs | 8 +- crates/kas-widgets/src/view/matrix_view.rs | 12 +- crates/kas-widgets/src/view/mod.rs | 11 +- crates/kas-widgets/src/view/single_view.rs | 27 ++-- examples/data-list-view.rs | 23 +-- examples/gallery.rs | 4 +- examples/times-tables.rs | 28 ++-- 15 files changed, 173 insertions(+), 247 deletions(-) diff --git a/crates/kas-core/src/model/data_impls.rs b/crates/kas-core/src/model/data_impls.rs index 528dfcc70..a5c0a72e7 100644 --- a/crates/kas-core/src/model/data_impls.rs +++ b/crates/kas-core/src/model/data_impls.rs @@ -12,7 +12,7 @@ use std::fmt::Debug; macro_rules! impl_list_data { ($ty:ty) => { - impl ListData for $ty { + impl SharedData for $ty { type Key = usize; type Item = T; @@ -20,17 +20,6 @@ macro_rules! impl_list_data { 1 } - fn len(&self) -> usize { - (*self).len() - } - - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - parent.make_child(*key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - child.next_key_after(parent) - } - fn contains_key(&self, key: &Self::Key) -> bool { *key < self.len() } @@ -42,6 +31,27 @@ macro_rules! impl_list_data { fn update(&self, _: &mut EventMgr, _: &Self::Key, _: Self::Item) { // Note: plain [T] does not support update, but SharedRc<[T]> does. } + } + impl SharedDataMut for $ty { + fn set(&mut self, key: &Self::Key, item: Self::Item) { + self[*key] = item; + } + } + impl ListData for $ty { + fn is_empty(&self) -> bool { + (*self).is_empty() + } + + fn len(&self) -> usize { + (*self).len() + } + + fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { + parent.make_child(*key) + } + fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { + child.next_key_after(parent) + } fn iter_vec(&self, limit: usize) -> Vec { (0..limit.min((*self).len())).collect() @@ -52,11 +62,6 @@ macro_rules! impl_list_data { (start.min(len)..(start + limit).min(len)).collect() } } - impl ListDataMut for $ty { - fn set(&mut self, key: &Self::Key, item: Self::Item) { - self[*key] = item; - } - } }; } diff --git a/crates/kas-core/src/model/data_traits.rs b/crates/kas-core/src/model/data_traits.rs index 266d46de7..d3b950b92 100644 --- a/crates/kas-core/src/model/data_traits.rs +++ b/crates/kas-core/src/model/data_traits.rs @@ -14,10 +14,15 @@ use crate::WidgetId; use std::cell::RefCell; use std::fmt::Debug; -/// Trait for viewable single data items +/// Trait for shared data +/// +/// By design, all methods take only `&self`. See also [`SharedDataMut`]. #[autoimpl(for &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] -pub trait SingleData: Debug { - /// Output type +pub trait SharedData: Debug { + /// Key type + type Key: Clone + Debug + PartialEq + Eq + 'static; + + /// Item type type Item: Clone + Debug + 'static; /// Get the data version @@ -31,10 +36,13 @@ pub trait SingleData: Debug { /// update. fn version(&self) -> u64; - // TODO(gat): add borrow<'a>(&self) -> Self::ItemRef<'a>, try_borrow? + /// Check whether a key has data + fn contains_key(&self, key: &Self::Key) -> bool; + + // TODO(gat): add borrow<'a>(&self, key: &Self::Key) -> Self::ItemRef<'a>, try_borrow? - /// Get data (clone) - fn get_cloned(&self) -> Self::Item; + /// Get data by key (clone) + fn get_cloned(&self, key: &Self::Key) -> Option; /// Update data, if supported /// @@ -43,7 +51,7 @@ pub trait SingleData: Debug { /// [`EventMgr::update_all`]. /// /// Data types without internal mutability should do nothing. - fn update(&self, mgr: &mut EventMgr, value: Self::Item); + fn update(&self, mgr: &mut EventMgr, key: &Self::Key, item: Self::Item); /// Handle a message from a widget /// @@ -52,47 +60,29 @@ pub trait SingleData: Debug { /// /// The default implementation attempts to extract a value of type /// [`Self::Item`], passing this to [`Self::update`] on success. - fn handle_message(&self, mgr: &mut EventMgr) { - if let Some(value) = mgr.try_pop_msg() { - self.update(mgr, value); + fn handle_message(&self, mgr: &mut EventMgr, key: &Self::Key) { + if let Some(item) = mgr.try_pop_msg() { + self.update(mgr, key, item); } } } -/// Trait for writable single data items +/// Trait for shared data with access via mutable reference #[autoimpl(for &mut T, Box)] -pub trait SingleDataMut: SingleData { +pub trait SharedDataMut: SharedData { // TODO(gat): add borrow_mut<'a>(&self) -> Self::ItemMutRef<'a>, try_borrow_mut? - /// Set data, given a mutable (unique) reference + /// Set data for an existing key /// /// It can be assumed that no synchronisation is required when a mutable - /// reference can be obtained, thus it is not required to increase the - /// version number or trigger an update. - fn set(&mut self, value: Self::Item); + /// reference can be obtained. The `version` number need not be affected. + fn set(&mut self, key: &Self::Key, item: Self::Item); } /// Trait for viewable data lists #[allow(clippy::len_without_is_empty)] #[autoimpl(for &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] -pub trait ListData: Debug { - /// Key type - type Key: Clone + Debug + PartialEq + Eq + 'static; - - /// Item type - type Item: Clone + Debug + 'static; - - /// Get the data version - /// - /// The version is increased on change and may be used to detect when views - /// over the data need to be refreshed. The initial version number must be - /// at least 1 (allowing 0 to represent an uninitialized state). - /// - /// Whenever the data is updated, [`Event::Update`] must be sent via - /// [`EventMgr::update_all`] to notify other users of this data of the - /// update. - fn version(&self) -> u64; - +pub trait ListData: SharedData { /// No data is available fn is_empty(&self) -> bool { self.len() == 0 @@ -117,36 +107,6 @@ pub trait ListData: Debug { /// the `key` passed to `make_id`. fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option; - // TODO(gat): add get<'a>(&self) -> Self::ItemRef<'a> and get_mut - - /// Check whether a key has data - fn contains_key(&self, key: &Self::Key) -> bool; - - /// Get data by key (clone) - fn get_cloned(&self, key: &Self::Key) -> Option; - - /// Update data, if supported - /// - /// Shared data with internal mutability (e.g. via [`RefCell`]) should - /// update itself here, increase its version number and call - /// [`EventMgr::update_all`]. - /// - /// Data types without internal mutability should do nothing. - fn update(&self, mgr: &mut EventMgr, key: &Self::Key, value: Self::Item); - - /// Handle a message from a widget - /// - /// This method is called when a view widget returns with a message. - /// It may use [`EventMgr::try_pop_msg`] and update self. - /// - /// The default implementation attempts to extract a value of type - /// [`Self::Item`], passing this to [`Self::update`] on success. - fn handle_message(&self, mgr: &mut EventMgr, key: &Self::Key) { - if let Some(value) = mgr.try_pop_msg() { - self.update(mgr, key, value); - } - } - // TODO(gat): replace with an iterator /// Iterate over keys as a vec /// @@ -163,40 +123,15 @@ pub trait ListData: Debug { fn iter_vec_from(&self, start: usize, limit: usize) -> Vec; } -/// Trait for writable data lists -#[autoimpl(for &mut T, Box)] -pub trait ListDataMut: ListData { - /// Set data for an existing key - /// - /// It can be assumed that no synchronisation is required when a mutable - /// reference can be obtained. The `version` number need not be affected. - fn set(&mut self, key: &Self::Key, item: Self::Item); -} - /// Trait for viewable data matrices /// /// Data matrices are a kind of table where each cell has the same type. #[autoimpl(for &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] -pub trait MatrixData: Debug { +pub trait MatrixData: SharedData { /// Column key type type ColKey: Clone + Debug + PartialEq + Eq + 'static; /// Row key type type RowKey: Clone + Debug + PartialEq + Eq + 'static; - /// Full key type - type Key: Clone + Debug + PartialEq + Eq + 'static; - /// Item type - type Item: Clone + Debug + 'static; - - /// Get the data version - /// - /// The version is increased on change and may be used to detect when views - /// over the data need to be refreshed. The initial version number must be - /// at least 1 (allowing 0 to represent an uninitialized state). - /// - /// Whenever the data is updated, [`Event::Update`] must be sent via - /// [`EventMgr::update_all`] to notify other users of this data of the - /// update. - fn version(&self) -> u64; /// No data is available fn is_empty(&self) -> bool; @@ -220,36 +155,6 @@ pub trait MatrixData: Debug { /// the `key` passed to `make_id`. fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option; - /// Check whether an item with these keys exists - fn contains(&self, key: &Self::Key) -> bool; - - /// Get data by key (clone) - /// - /// It is expected that this method succeeds when both keys are valid. - fn get_cloned(&self, key: &Self::Key) -> Option; - - /// Update data, if supported - /// - /// Shared data with internal mutability (e.g. via [`RefCell`]) should - /// update itself here, increase its version number and call - /// [`EventMgr::update_all`]. - /// - /// Data types without internal mutability should do nothing. - fn update(&self, mgr: &mut EventMgr, key: &Self::Key, value: Self::Item); - - /// Handle a message from a widget - /// - /// This method is called when a view widget returns with a message. - /// It may use [`EventMgr::try_pop_msg`] and update self. - /// - /// The default implementation attempts to extract a value of type - /// [`Self::Item`], passing this to [`Self::update`] on success. - fn handle_message(&self, mgr: &mut EventMgr, key: &Self::Key) { - if let Some(value) = mgr.try_pop_msg() { - self.update(mgr, key, value); - } - } - // TODO(gat): replace with an iterator /// Iterate over column keys as a vec /// @@ -280,13 +185,3 @@ pub trait MatrixData: Debug { /// Make a key from parts fn make_key(col: &Self::ColKey, row: &Self::RowKey) -> Self::Key; } - -/// Trait for writable data matrices -#[autoimpl(for &mut T, Box)] -pub trait MatrixDataMut: MatrixData { - /// Set data for an existing cell - /// - /// It can be assumed that no synchronisation is required when a mutable - /// reference can be obtained. The `version` number need not be affected. - fn set(&mut self, key: &Self::Key, item: Self::Item); -} diff --git a/crates/kas-core/src/model/filter.rs b/crates/kas-core/src/model/filter.rs index 328f33010..c2473a32b 100644 --- a/crates/kas-core/src/model/filter.rs +++ b/crates/kas-core/src/model/filter.rs @@ -30,25 +30,30 @@ impl ContainsString { ContainsString(Rc::new((id, data))) } } -impl SingleData for ContainsString { +impl SharedData for ContainsString { + type Key = (); type Item = String; fn version(&self) -> u64 { (self.0).1.borrow().1 } - fn get_cloned(&self) -> Self::Item { - (self.0).1.borrow().0.to_owned() + fn contains_key(&self, _: &Self::Key) -> bool { + true } - fn update(&self, mgr: &mut EventMgr, value: Self::Item) { + + fn get_cloned(&self, _: &Self::Key) -> Option { + Some((self.0).1.borrow().0.to_owned()) + } + fn update(&self, mgr: &mut EventMgr, _: &Self::Key, value: Self::Item) { let mut cell = (self.0).1.borrow_mut(); cell.0 = value; cell.1 += 1; mgr.update_all((self.0).0, 0); } } -impl SingleDataMut for ContainsString { - fn set(&mut self, value: Self::Item) { +impl SharedDataMut for ContainsString { + fn set(&mut self, _: &Self::Key, value: Self::Item) { let mut cell = (self.0).1.borrow_mut(); cell.0 = value; cell.1 += 1; @@ -86,17 +91,22 @@ impl ContainsCaseInsensitive { ContainsCaseInsensitive(Rc::new((id, data))) } } -impl SingleData for ContainsCaseInsensitive { +impl SharedData for ContainsCaseInsensitive { + type Key = (); type Item = String; fn version(&self) -> u64 { (self.0).1.borrow().2 } - fn get_cloned(&self) -> Self::Item { - (self.0).1.borrow().0.clone() + fn contains_key(&self, _: &Self::Key) -> bool { + true + } + + fn get_cloned(&self, _: &Self::Key) -> Option { + Some((self.0).1.borrow().0.clone()) } - fn update(&self, mgr: &mut EventMgr, value: Self::Item) { + fn update(&self, mgr: &mut EventMgr, _: &Self::Key, value: Self::Item) { let mut cell = (self.0).1.borrow_mut(); cell.0 = value; cell.1 = cell.0.to_uppercase(); @@ -104,8 +114,8 @@ impl SingleData for ContainsCaseInsensitive { mgr.update_all((self.0).0, 0); } } -impl SingleDataMut for ContainsCaseInsensitive { - fn set(&mut self, value: Self::Item) { +impl SharedDataMut for ContainsCaseInsensitive { + fn set(&mut self, _: &Self::Key, value: Self::Item) { let mut cell = (self.0).1.borrow_mut(); cell.0 = value; cell.1 = cell.0.to_uppercase(); diff --git a/crates/kas-core/src/model/mod.rs b/crates/kas-core/src/model/mod.rs index bbb12f74b..97f61d819 100644 --- a/crates/kas-core/src/model/mod.rs +++ b/crates/kas-core/src/model/mod.rs @@ -13,7 +13,5 @@ mod data_traits; pub mod filter; mod shared_rc; -pub use data_traits::{ - ListData, ListDataMut, MatrixData, MatrixDataMut, SingleData, SingleDataMut, -}; +pub use data_traits::{ListData, MatrixData, SharedData, SharedDataMut}; pub use shared_rc::{SharedRc, SharedRcRef}; diff --git a/crates/kas-core/src/model/shared_rc.rs b/crates/kas-core/src/model/shared_rc.rs index 0ce94e975..d44630953 100644 --- a/crates/kas-core/src/model/shared_rc.rs +++ b/crates/kas-core/src/model/shared_rc.rs @@ -24,9 +24,9 @@ use std::rc::Rc; /// version counter. /// /// The wrapped value may be read via [`Self::borrow`], [`Self::try_borrow`] and -/// [`SingleData::get_cloned`]. +/// [`SharedData::get_cloned`]. /// -/// The value may be set via [`SingleData::update`] and [`SingleDataMut::set`]. +/// The value may be set via [`SharedData::update`] and [`SharedDataMut::set`]. /// /// This wrapper type may be useful for simple shared data, but for more complex /// uses a custom wrapper type may be required. @@ -82,26 +82,32 @@ impl SharedRc { } } -impl SingleData for SharedRc { +impl SharedData for SharedRc { + type Key = (); type Item = T; fn version(&self) -> u64 { (self.0).1.borrow().1 } - fn get_cloned(&self) -> Self::Item { - (self.0).1.borrow().0.to_owned() + fn contains_key(&self, _: &()) -> bool { + true } - fn update(&self, mgr: &mut EventMgr, value: Self::Item) { + fn get_cloned(&self, _: &()) -> Option { + Some((self.0).1.borrow().0.clone()) + } + + fn update(&self, mgr: &mut EventMgr, _: &(), item: Self::Item) { let mut cell = (self.0).1.borrow_mut(); - cell.0 = value; + cell.0 = item; cell.1 += 1; mgr.update_all((self.0).0, 0); } } -impl SingleDataMut for SharedRc { - fn set(&mut self, value: Self::Item) { - (self.0).1.borrow_mut().0 = value; + +impl SharedDataMut for SharedRc { + fn set(&mut self, _: &(), item: Self::Item) { + (self.0).1.borrow_mut().0 = item; } } diff --git a/crates/kas-widgets/src/dialog.rs b/crates/kas-widgets/src/dialog.rs index 7313c794f..8ed31f837 100644 --- a/crates/kas-widgets/src/dialog.rs +++ b/crates/kas-widgets/src/dialog.rs @@ -10,7 +10,7 @@ use crate::{EditBox, Filler, Label, TextButton}; use kas::event::{Command, VirtualKeyCode}; -use kas::model::{SharedRc, SingleData}; +use kas::model::{SharedData, SharedRc}; use kas::prelude::*; use kas::text::format::FormattableText; use kas::{Icon, Widget}; @@ -144,7 +144,7 @@ impl_scope! { }; }] /// An editor over a shared `String` - pub struct TextEdit = SharedRc> { + pub struct TextEdit = SharedRc> { core: widget_core!(), title: Cow<'static, str>, data: T, @@ -155,7 +155,7 @@ impl_scope! { impl Self { /// Construct pub fn new(title: impl Into>, multi_line: bool, data: T) -> Self { - let text = data.get_cloned(); + let text = data.get_cloned(&()).unwrap(); TextEdit { core: Default::default(), title: title.into(), @@ -166,7 +166,7 @@ impl_scope! { fn close(&mut self, mgr: &mut EventMgr, commit: bool) -> Response { if commit { - self.data.update(mgr, self.edit.get_string()); + self.data.update(mgr, &(), self.edit.get_string()); } mgr.send_action(TkAction::CLOSE); Response::Used diff --git a/crates/kas-widgets/src/radio_box.rs b/crates/kas-widgets/src/radio_box.rs index 6f17f5b13..66040175e 100644 --- a/crates/kas-widgets/src/radio_box.rs +++ b/crates/kas-widgets/src/radio_box.rs @@ -6,7 +6,7 @@ //! Toggle widgets use super::AccelLabel; -use kas::model::{SharedRc, SingleData}; +use kas::model::{SharedData, SharedRc}; use kas::prelude::*; use kas::theme::Feature; use log::trace; @@ -41,7 +41,7 @@ impl_scope! { fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { match event { Event::Update { id, .. } if id == self.group.id() => { - if self.state && !self.eq_id(self.group.get_cloned()) { + if self.state && !self.eq_id(self.group.get_cloned(&()).unwrap()) { trace!("RadioBox: unset {}", self.id()); self.state = false; self.last_change = Some(Instant::now()); @@ -149,7 +149,7 @@ impl_scope! { self.state = true; self.last_change = Some(Instant::now()); mgr.redraw(self.id()); - self.group.update(mgr, Some(self.id())); + self.group.update(mgr, &(), Some(self.id())); true } else { false @@ -161,7 +161,7 @@ impl_scope! { /// Note: state will not update until the next draw. #[inline] pub fn unset_all(&self, mgr: &mut EventMgr) { - self.group.update(mgr, None); + self.group.update(mgr, &(), None); } } diff --git a/crates/kas-widgets/src/view/filter_list.rs b/crates/kas-widgets/src/view/filter_list.rs index f121ba949..934533d63 100644 --- a/crates/kas-widgets/src/view/filter_list.rs +++ b/crates/kas-widgets/src/view/filter_list.rs @@ -6,7 +6,7 @@ //! Filter-list view widget use kas::model::filter::Filter; -use kas::model::{ListData, SingleData}; +use kas::model::{ListData, SharedData}; use kas::prelude::*; use std::cell::RefCell; use std::fmt::Debug; @@ -25,7 +25,7 @@ use std::fmt::Debug; /// Warning: this implementation is `O(n)` where `n = data.len()` and not well /// optimised, thus is expected to be slow on large data lists. #[derive(Clone, Debug)] -pub struct FilteredList + SingleData> { +pub struct FilteredList + SharedData> { /// Direct access to unfiltered data /// /// If adjusting this, one should call [`FilteredList::refresh`] after. @@ -37,7 +37,7 @@ pub struct FilteredList + SingleData> { view: RefCell<(u64, Vec)>, } -impl + SingleData> FilteredList { +impl + SharedData> FilteredList { /// Construct from `data` and a `filter` #[inline] pub fn new(data: T, filter: F) -> Self { @@ -64,7 +64,7 @@ impl + SingleData> FilteredList { } } -impl + SingleData> ListData for FilteredList { +impl + SharedData> SharedData for FilteredList { type Key = T::Key; type Item = T::Item; @@ -76,16 +76,6 @@ impl + SingleData> ListData for FilteredList usize { - self.view.borrow().1.len() - } - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - self.data.make_id(parent, key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - self.data.reconstruct_key(parent, child) - } - fn contains_key(&self, key: &Self::Key) -> bool { self.get_cloned(key).is_some() } @@ -112,6 +102,21 @@ impl + SingleData> ListData for FilteredList + SharedData> ListData for FilteredList { + fn is_empty(&self) -> bool { + self.view.borrow().1.is_empty() + } + fn len(&self) -> usize { + self.view.borrow().1.len() + } + fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { + self.data.make_id(parent, key) + } + fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { + self.data.reconstruct_key(parent, child) + } fn iter_vec_from(&self, start: usize, limit: usize) -> Vec { let end = self.len().min(start + limit); diff --git a/crates/kas-widgets/src/view/list_view.rs b/crates/kas-widgets/src/view/list_view.rs index f43a3945b..39c16bc9f 100644 --- a/crates/kas-widgets/src/view/list_view.rs +++ b/crates/kas-widgets/src/view/list_view.rs @@ -13,6 +13,8 @@ use kas::event::components::ScrollComponent; use kas::event::{Command, CursorIcon, Scroll}; use kas::layout::solve_size_rules; use kas::model::ListData; +#[allow(unused)] +use kas::model::SharedData; use kas::prelude::*; use linear_map::set::LinearSet; use log::{debug, trace}; @@ -42,8 +44,8 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`ListData::handle_message`] method is - /// called. After calling [`ListData::handle_message`], this widget attempts to + /// When a child pushes a message, the [`SharedData::handle_message`] method is + /// called. After calling [`SharedData::handle_message`], this widget attempts to /// read and handle [`SelectMsg`]. /// /// When selection is enabled and an item is selected or deselected, this @@ -160,7 +162,7 @@ impl_scope! { /// Set shared data /// /// This method updates the shared data, if supported (see - /// [`ListData::update`]). Other widgets sharing this data are notified + /// [`SharedData::update`]). Other widgets sharing this data are notified /// of the update, if data is changed. pub fn set_value(&self, mgr: &mut EventMgr, key: &T::Key, data: T::Item) { self.data.update(mgr, key, data); diff --git a/crates/kas-widgets/src/view/matrix_view.rs b/crates/kas-widgets/src/view/matrix_view.rs index 69deeb908..ddf0ae53a 100644 --- a/crates/kas-widgets/src/view/matrix_view.rs +++ b/crates/kas-widgets/src/view/matrix_view.rs @@ -13,6 +13,8 @@ use kas::event::components::ScrollComponent; use kas::event::{Command, CursorIcon, Scroll}; use kas::layout::solve_size_rules; use kas::model::MatrixData; +#[allow(unused)] +use kas::model::SharedData; use kas::prelude::*; use linear_map::set::LinearSet; use log::{debug, trace}; @@ -48,8 +50,8 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`MatrixData::handle_message`] method is - /// called. After calling [`MatrixData::handle_message`], this widget attempts + /// When a child pushes a message, the [`SharedData::handle_message`] method is + /// called. After calling [`SharedData::handle_message`], this widget attempts /// to read and handle [`SelectMsg`]. /// /// When selection is enabled and an item is selected or deselected, this @@ -142,7 +144,7 @@ impl_scope! { /// Set shared data /// /// This method updates the shared data, if supported (see - /// [`MatrixData::update`]). Other widgets sharing this data are notified + /// [`SharedData::update`]). Other widgets sharing this data are notified /// of the update, if data is changed. pub fn set_value(&self, mgr: &mut EventMgr, key: &T::Key, data: T::Item) { self.data.update(mgr, key, data); @@ -221,7 +223,7 @@ impl_scope! { SelectionMode::Single => self.selection.clear(), _ => (), } - if !self.data.contains(&key) { + if !self.data.contains_key(&key) { return Err(SelectionError::Key); } match self.selection.insert(key) { @@ -244,7 +246,7 @@ impl_scope! { /// Manually trigger an update to handle changed data pub fn update_view(&mut self, mgr: &mut EventMgr) { let data = &self.data; - self.selection.retain(|key| data.contains(key)); + self.selection.retain(|key| data.contains_key(key)); for w in &mut self.widgets { w.key = None; } diff --git a/crates/kas-widgets/src/view/mod.rs b/crates/kas-widgets/src/view/mod.rs index 153d7d493..530327782 100644 --- a/crates/kas-widgets/src/view/mod.rs +++ b/crates/kas-widgets/src/view/mod.rs @@ -12,12 +12,11 @@ //! //! # Shared data and *model* //! -//! Shared data must implement several traits, namely those in -//! [`kas::model`] and one of the "view" traits: [`SingleData`], -//! [`ListData`] or [`MatrixData`]. These traits together form the "model". +//! Shared data must implement [`SharedData`], optionally [`SharedDataMut`] +//! and (depending on dimension) optionally [`ListData`] or [`MatrixData`]. //! //! For simpler cases it is not always necessary to implement your own shared -//! data type, for example `SharedRc` implements [`SingleData`] and +//! data type, for example `SharedRc` implements [`SharedData`] TODO and //! `&'static [&'static str]` implements [`ListData`]. The [`SharedRc`] type //! provides an `update` method and the [`UpdateId`] and version counter //! required to synchronise views; `&[T]` does not (data is constant). @@ -61,14 +60,14 @@ //! //! The following views are provided: //! -//! - [`SingleView`] creates a view over a [`SingleData`] object (no scrolling +//! - [`SingleView`] creates a view over a [`SharedData`] object (no scrolling //! or selection support) //! - [`ListView`] creates a scrollable list view over a [`ListData`] object #[allow(unused)] use kas::event::UpdateId; #[allow(unused)] -use kas::model::{ListData, MatrixData, SharedRc, SingleData}; +use kas::model::{ListData, MatrixData, SharedData, SharedDataMut, SharedRc}; use thiserror::Error; mod filter_list; diff --git a/crates/kas-widgets/src/view/single_view.rs b/crates/kas-widgets/src/view/single_view.rs index fad55156d..4ef716d34 100644 --- a/crates/kas-widgets/src/view/single_view.rs +++ b/crates/kas-widgets/src/view/single_view.rs @@ -6,7 +6,7 @@ //! Single view widget use super::{driver, Driver}; -use kas::model::SingleData; +use kas::model::SharedData; use kas::prelude::*; impl_scope! { @@ -14,17 +14,17 @@ impl_scope! { /// /// This widget supports a view over a shared data item. /// - /// The shared data type `T` must support [`SingleData`]. + /// The shared data type `T` must support [`SharedData`]. /// One may use [`kas::model::SharedRc`] /// or a custom shared data type. /// - /// The driver `V` must implement [`Driver`], with data type - /// `::Item`. Several implementations are available in the + /// The driver `V` must implement [`Driver`] over data type + /// `::Item`. Several implementations are available in the /// [`driver`] module or a custom implementation may be used. /// /// # Messages /// - /// When a child pushes a message, the [`SingleData::handle_message`] method is + /// When a child pushes a message, the [`SharedData::handle_message`] method is /// called. #[autoimpl(Debug ignore self.view)] #[derive(Clone)] @@ -32,7 +32,7 @@ impl_scope! { layout = self.child; }] pub struct SingleView< - T: SingleData, + T: SharedData, V: Driver = driver::DefaultView, > { core: widget_core!(), @@ -84,16 +84,16 @@ impl_scope! { /// Get a copy of the shared value pub fn get_value(&self) -> T::Item { - self.data.get_cloned() + self.data.get_cloned(&()).unwrap() } /// Set shared data /// /// This method updates the shared data, if supported (see - /// [`SingleData::update`]). Other widgets sharing this data are notified + /// [`SharedData::update`]). Other widgets sharing this data are notified /// of the update, if data is changed. pub fn set_value(&self, mgr: &mut EventMgr, data: T::Item) { - self.data.update(mgr, data); + self.data.update(mgr, &(), data); } /// Update shared data @@ -108,7 +108,8 @@ impl_scope! { impl Widget for Self { fn configure(&mut self, mgr: &mut ConfigMgr) { // We set data now, after child is configured - *mgr |= self.view.set(&mut self.child, self.data.get_cloned()); + let item = self.data.get_cloned(&()).unwrap(); + *mgr |= self.view.set(&mut self.child, item); } fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { @@ -116,8 +117,8 @@ impl_scope! { Event::Update { .. } => { let data_ver = self.data.version(); if data_ver > self.data_ver { - let value = self.data.get_cloned(); - *mgr |= self.view.set(&mut self.child, value); + let item = self.data.get_cloned(&()).unwrap(); + *mgr |= self.view.set(&mut self.child, item); self.data_ver = data_ver; } Response::Used @@ -127,7 +128,7 @@ impl_scope! { } fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - self.data.handle_message(mgr); + self.data.handle_message(mgr, &()); } } } diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index ba1f6d9a3..2d0500fef 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -85,7 +85,7 @@ impl MySharedData { (new_text, self.id) } } -impl ListData for MySharedData { +impl SharedData for MySharedData { type Key = usize; type Item = (usize, bool, String); @@ -93,16 +93,6 @@ impl ListData for MySharedData { self.data.borrow().ver } - fn len(&self) -> usize { - self.data.borrow().len - } - fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { - parent.make_child(*key) - } - fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { - child.next_key_after(parent) - } - fn contains_key(&self, key: &Self::Key) -> bool { *key < self.len() } @@ -133,6 +123,17 @@ impl ListData for MySharedData { mgr.update_all(self.id, 0); } } +} +impl ListData for MySharedData { + fn len(&self) -> usize { + self.data.borrow().len + } + fn make_id(&self, parent: &WidgetId, key: &Self::Key) -> WidgetId { + parent.make_child(*key) + } + fn reconstruct_key(&self, parent: &WidgetId, child: &WidgetId) -> Option { + child.next_key_after(parent) + } fn iter_vec(&self, limit: usize) -> Vec { (0..limit.min(self.len())).collect() diff --git a/examples/gallery.rs b/examples/gallery.rs index 4b70aab36..c05c3548b 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -270,7 +270,7 @@ Demonstration of *as-you-type* formatting from **Markdown**. fn filter_list() -> Box { use kas::dir::Down; - use kas::model::{filter::ContainsCaseInsensitive, SingleData}; + use kas::model::{filter::ContainsCaseInsensitive, SharedData}; use kas::widgets::view::{driver, SelectionMode, SelectionMsg}; const MONTHS: &[&str] = &[ @@ -313,7 +313,7 @@ fn filter_list() -> Box { #[widget] r1 = RadioButton::new_msg("single", r.clone(), SelectionMode::Single), #[widget] r2 = RadioButton::new_msg("multiple", r, SelectionMode::Multiple), #[widget] filter = EditBox::new("") - .on_edit(move |s, mgr| filter.update(mgr, s.to_string())), + .on_edit(move |s, mgr| filter.update(mgr, &(), s.to_string())), #[widget] list: ScrollBars = ScrollBars::new(ListView::new(filtered)) } diff --git a/examples/times-tables.rs b/examples/times-tables.rs index 90ae05c54..92c8531b5 100644 --- a/examples/times-tables.rs +++ b/examples/times-tables.rs @@ -1,15 +1,13 @@ //! Do you know your times tables? -use kas::model::MatrixData; +use kas::model::{MatrixData, SharedData}; use kas::prelude::*; use kas::widgets::view::{driver::DefaultNav, MatrixView, SelectionMode}; use kas::widgets::{EditBox, ScrollBars}; #[derive(Debug)] struct TableData(u64, usize); -impl MatrixData for TableData { - type ColKey = usize; - type RowKey = usize; +impl SharedData for TableData { type Key = (usize, usize); type Item = usize; @@ -17,6 +15,19 @@ impl MatrixData for TableData { self.0 } + fn contains_key(&self, key: &Self::Key) -> bool { + key.0 < self.1 && key.1 < self.1 + } + fn get_cloned(&self, key: &Self::Key) -> Option { + self.contains_key(key).then(|| (key.0 + 1) * (key.1 + 1)) + } + + fn update(&self, _: &mut EventMgr, _: &Self::Key, _: Self::Item) {} +} +impl MatrixData for TableData { + type ColKey = usize; + type RowKey = usize; + fn is_empty(&self) -> bool { self.1 == 0 } @@ -34,15 +45,6 @@ impl MatrixData for TableData { col.zip(row) } - fn contains(&self, key: &Self::Key) -> bool { - key.0 < self.1 && key.1 < self.1 - } - fn get_cloned(&self, key: &Self::Key) -> Option { - self.contains(key).then(|| (key.0 + 1) * (key.1 + 1)) - } - - fn update(&self, _: &mut EventMgr, _: &Self::Key, _: Self::Item) {} - fn col_iter_vec_from(&self, start: usize, limit: usize) -> Vec { (start..(start + limit)).collect() } From 9b2b358d43d3e92f4c02a0a5c5af438b886af0c5 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 17:15:04 +0100 Subject: [PATCH 13/27] Add kas::model::DataKey, SingleData --- crates/kas-core/src/model/data_traits.rs | 20 ++++++++++++++++---- crates/kas-core/src/model/mod.rs | 2 +- crates/kas-widgets/src/dialog.rs | 4 ++-- crates/kas-widgets/src/view/filter_list.rs | 10 +++++----- crates/kas-widgets/src/view/single_view.rs | 6 ++++-- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/kas-core/src/model/data_traits.rs b/crates/kas-core/src/model/data_traits.rs index d3b950b92..a234adb26 100644 --- a/crates/kas-core/src/model/data_traits.rs +++ b/crates/kas-core/src/model/data_traits.rs @@ -14,13 +14,18 @@ use crate::WidgetId; use std::cell::RefCell; use std::fmt::Debug; +/// Bounds on the key type +pub trait DataKey: Clone + Debug + PartialEq + Eq + 'static {} +impl DataKey for Key {} + /// Trait for shared data /// /// By design, all methods take only `&self`. See also [`SharedDataMut`]. -#[autoimpl(for &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] +#[autoimpl(for + &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] pub trait SharedData: Debug { /// Key type - type Key: Clone + Debug + PartialEq + Eq + 'static; + type Key: DataKey; /// Item type type Item: Clone + Debug + 'static; @@ -79,6 +84,13 @@ pub trait SharedDataMut: SharedData { fn set(&mut self, key: &Self::Key, item: Self::Item); } +/// Trait bound for viewable single data +/// +/// This is automatically implemented for every type implementing `SharedData<()>`. +// TODO(trait aliases): make this an actual trait alias +pub trait SingleData: SharedData {} +impl> SingleData for T {} + /// Trait for viewable data lists #[allow(clippy::len_without_is_empty)] #[autoimpl(for &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] @@ -129,9 +141,9 @@ pub trait ListData: SharedData { #[autoimpl(for &T, &mut T, std::rc::Rc, std::sync::Arc, Box)] pub trait MatrixData: SharedData { /// Column key type - type ColKey: Clone + Debug + PartialEq + Eq + 'static; + type ColKey: DataKey; /// Row key type - type RowKey: Clone + Debug + PartialEq + Eq + 'static; + type RowKey: DataKey; /// No data is available fn is_empty(&self) -> bool; diff --git a/crates/kas-core/src/model/mod.rs b/crates/kas-core/src/model/mod.rs index 97f61d819..c124f60de 100644 --- a/crates/kas-core/src/model/mod.rs +++ b/crates/kas-core/src/model/mod.rs @@ -13,5 +13,5 @@ mod data_traits; pub mod filter; mod shared_rc; -pub use data_traits::{ListData, MatrixData, SharedData, SharedDataMut}; +pub use data_traits::{DataKey, ListData, MatrixData, SharedData, SharedDataMut, SingleData}; pub use shared_rc::{SharedRc, SharedRcRef}; diff --git a/crates/kas-widgets/src/dialog.rs b/crates/kas-widgets/src/dialog.rs index 8ed31f837..6603ad5db 100644 --- a/crates/kas-widgets/src/dialog.rs +++ b/crates/kas-widgets/src/dialog.rs @@ -10,7 +10,7 @@ use crate::{EditBox, Filler, Label, TextButton}; use kas::event::{Command, VirtualKeyCode}; -use kas::model::{SharedData, SharedRc}; +use kas::model::{SharedRc, SingleData}; use kas::prelude::*; use kas::text::format::FormattableText; use kas::{Icon, Widget}; @@ -144,7 +144,7 @@ impl_scope! { }; }] /// An editor over a shared `String` - pub struct TextEdit = SharedRc> { + pub struct TextEdit = SharedRc> { core: widget_core!(), title: Cow<'static, str>, data: T, diff --git a/crates/kas-widgets/src/view/filter_list.rs b/crates/kas-widgets/src/view/filter_list.rs index 934533d63..392518c24 100644 --- a/crates/kas-widgets/src/view/filter_list.rs +++ b/crates/kas-widgets/src/view/filter_list.rs @@ -6,7 +6,7 @@ //! Filter-list view widget use kas::model::filter::Filter; -use kas::model::{ListData, SharedData}; +use kas::model::{ListData, SharedData, SingleData}; use kas::prelude::*; use std::cell::RefCell; use std::fmt::Debug; @@ -25,7 +25,7 @@ use std::fmt::Debug; /// Warning: this implementation is `O(n)` where `n = data.len()` and not well /// optimised, thus is expected to be slow on large data lists. #[derive(Clone, Debug)] -pub struct FilteredList + SharedData> { +pub struct FilteredList + SingleData> { /// Direct access to unfiltered data /// /// If adjusting this, one should call [`FilteredList::refresh`] after. @@ -37,7 +37,7 @@ pub struct FilteredList + SharedData> { view: RefCell<(u64, Vec)>, } -impl + SharedData> FilteredList { +impl + SingleData> FilteredList { /// Construct from `data` and a `filter` #[inline] pub fn new(data: T, filter: F) -> Self { @@ -64,7 +64,7 @@ impl + SharedData> FilteredList { } } -impl + SharedData> SharedData for FilteredList { +impl + SingleData> SharedData for FilteredList { type Key = T::Key; type Item = T::Item; @@ -104,7 +104,7 @@ impl + SharedData> SharedData for FilteredList + SharedData> ListData for FilteredList { +impl + SingleData> ListData for FilteredList { fn is_empty(&self) -> bool { self.view.borrow().1.is_empty() } diff --git a/crates/kas-widgets/src/view/single_view.rs b/crates/kas-widgets/src/view/single_view.rs index 4ef716d34..5e30cbd8f 100644 --- a/crates/kas-widgets/src/view/single_view.rs +++ b/crates/kas-widgets/src/view/single_view.rs @@ -6,7 +6,9 @@ //! Single view widget use super::{driver, Driver}; +#[allow(unused)] use kas::model::SharedData; +use kas::model::SingleData; use kas::prelude::*; impl_scope! { @@ -14,7 +16,7 @@ impl_scope! { /// /// This widget supports a view over a shared data item. /// - /// The shared data type `T` must support [`SharedData`]. + /// The shared data type `T` must support [`SingleData`]. /// One may use [`kas::model::SharedRc`] /// or a custom shared data type. /// @@ -32,7 +34,7 @@ impl_scope! { layout = self.child; }] pub struct SingleView< - T: SharedData, + T: SingleData, V: Driver = driver::DefaultView, > { core: widget_core!(), From 414a52866be18578b41a1f1771315d56ef295a1f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 09:31:24 +0100 Subject: [PATCH 14/27] Add Data: SharedData type parameter to Driver Rationale: commit after next --- crates/kas-widgets/src/view/driver.rs | 136 ++++++++++++------- crates/kas-widgets/src/view/driver/config.rs | 8 +- crates/kas-widgets/src/view/list_view.rs | 19 +-- crates/kas-widgets/src/view/matrix_view.rs | 13 +- crates/kas-widgets/src/view/single_view.rs | 12 +- examples/data-list-view.rs | 17 ++- 6 files changed, 126 insertions(+), 79 deletions(-) diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index 14de12951..8d2d8bab1 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -14,6 +14,7 @@ use crate::{ CheckBox, EditBox, EditField, EditGuard, Label, NavFrame, ProgressBar, RadioGroup, SliderValue, SpinnerValue, }; +use kas::model::SharedData; use kas::prelude::*; use std::default::Default; use std::fmt::Debug; @@ -33,7 +34,11 @@ use std::ops::RangeInclusive; /// /// - [`DefaultView`] will choose a sensible widget to view the data /// - [`DefaultNav`] will choose a sensible widget to view the data -pub trait Driver: Debug { +/// +/// NOTE: `Item` is a direct type parameter (in addition to an assoc. type +/// param. of `SharedData`) only to avoid "conflicting implementations" errors. +/// Similar to: rust#20400, rust#92894. Given fixes, we may remove the param. +pub trait Driver>: Debug { /// Type of the widget used to view data type Widget: kas::Widget; @@ -42,11 +47,12 @@ pub trait Driver: Debug { /// Such instances are used for sizing and cached widgets, but not shown. /// The controller may later call [`Driver::set`] on the widget then show it. fn make(&self) -> Self::Widget; + /// Set the viewed data /// /// The widget may expect `configure` to be called at least once before data /// is set and to have `set_rect` called after each time data is set. - fn set(&self, widget: &mut Self::Widget, data: T) -> TkAction; + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction; } /// Default view widget constructor @@ -71,22 +77,22 @@ pub struct DefaultNav; macro_rules! impl_via_to_string { ($t:ty) => { - impl Driver<$t> for DefaultView { + impl> Driver<$t, Data> for DefaultView { type Widget = Label; fn make(&self) -> Self::Widget { Label::new("".to_string()) } - fn set(&self, widget: &mut Self::Widget, data: $t) -> TkAction { - widget.set_string(data.to_string()) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key).map(|item| widget.set_string(item.to_string())).unwrap_or(TkAction::EMPTY) } } - impl Driver<$t> for DefaultNav { + impl> Driver<$t, Data> for DefaultNav { type Widget = NavFrame>; fn make(&self) -> Self::Widget { NavFrame::new(Label::new("".to_string())) } - fn set(&self, widget: &mut Self::Widget, data: $t) -> TkAction { - widget.set_string(data.to_string()) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key).map(|item| widget.set_string(item.to_string())).unwrap_or(TkAction::EMPTY) } } }; @@ -100,23 +106,27 @@ impl_via_to_string!(i8, i16, i32, i64, i128, isize); impl_via_to_string!(u8, u16, u32, u64, u128, usize); impl_via_to_string!(f32, f64); -impl Driver for DefaultView { +impl> Driver for DefaultView { type Widget = CheckBox; fn make(&self) -> Self::Widget { - CheckBox::new().with_editable(false) + CheckBox::new_on(|mgr, state| mgr.push_msg(state)).with_editable(false) } - fn set(&self, widget: &mut Self::Widget, data: bool) -> TkAction { - widget.set_bool(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_bool(item)) + .unwrap_or(TkAction::EMPTY) } } -impl Driver for DefaultNav { +impl> Driver for DefaultNav { type Widget = CheckBox; fn make(&self) -> Self::Widget { CheckBox::new().with_editable(false) } - fn set(&self, widget: &mut Self::Widget, data: bool) -> TkAction { - widget.set_bool(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_bool(item)) + .unwrap_or(TkAction::EMPTY) } } @@ -136,47 +146,63 @@ impl Default for Widget { } } -impl Driver for Widget<>::Widget> +// NOTE: this implementation conflicts, where it did not before adding the Data +// type parameter to Driver. Possibly it can be re-enabled in the future. +/* +impl> Driver + for Widget<>::Widget> where - DefaultView: Driver, + DefaultView: Driver, { - type Widget = >::Widget; + type Widget = >::Widget; fn make(&self) -> Self::Widget { DefaultView.make() } - fn set(&self, widget: &mut Self::Widget, data: T) -> TkAction { - DefaultView.set(widget, data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + DefaultView.set(widget, data, key) } -} +}*/ -impl Driver for Widget> { +impl> Driver + for Widget> +{ type Widget = EditField; fn make(&self) -> Self::Widget { let guard = G::default(); EditField::new("".to_string()).with_guard(guard) } - fn set(&self, widget: &mut Self::Widget, data: String) -> TkAction { - widget.set_string(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_string(item)) + .unwrap_or(TkAction::EMPTY) } } -impl Driver for Widget> { +impl> Driver + for Widget> +{ type Widget = EditBox; fn make(&self) -> Self::Widget { let guard = G::default(); EditBox::new("".to_string()).with_guard(guard) } - fn set(&self, widget: &mut Self::Widget, data: String) -> TkAction { - widget.set_string(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_string(item)) + .unwrap_or(TkAction::EMPTY) } } -impl Driver for Widget> { +impl> Driver + for Widget> +{ type Widget = ProgressBar; fn make(&self) -> Self::Widget { ProgressBar::new() } - fn set(&self, widget: &mut Self::Widget, data: f32) -> TkAction { - widget.set_value(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_value(item)) + .unwrap_or(TkAction::EMPTY) } } @@ -192,13 +218,15 @@ impl CheckButton { CheckButton { label } } } -impl Driver for CheckButton { +impl> Driver for CheckButton { type Widget = crate::CheckButton; fn make(&self) -> Self::Widget { crate::CheckButton::new(self.label.clone()).on_toggle(|mgr, state| mgr.push_msg(state)) } - fn set(&self, widget: &mut Self::Widget, data: bool) -> TkAction { - widget.set_bool(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_bool(item)) + .unwrap_or(TkAction::EMPTY) } } @@ -213,13 +241,15 @@ impl RadioBox { RadioBox { group } } } -impl Driver for RadioBox { +impl> Driver for RadioBox { type Widget = crate::RadioBox; fn make(&self) -> Self::Widget { crate::RadioBox::new(self.group.clone()).on_select(|mgr| mgr.push_msg(true)) } - fn set(&self, widget: &mut Self::Widget, data: bool) -> TkAction { - widget.set_bool(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_bool(item)) + .unwrap_or(TkAction::EMPTY) } } @@ -236,14 +266,16 @@ impl RadioButton { RadioButton { label, group } } } -impl Driver for RadioButton { +impl> Driver for RadioButton { type Widget = crate::RadioButton; fn make(&self) -> Self::Widget { crate::RadioButton::new(self.label.clone(), self.group.clone()) .on_select(|mgr| mgr.push_msg(true)) } - fn set(&self, widget: &mut Self::Widget, data: bool) -> TkAction { - widget.set_bool(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_bool(item)) + .unwrap_or(TkAction::EMPTY) } } @@ -274,13 +306,18 @@ impl Slider { } } } -impl Driver for Slider { - type Widget = crate::Slider; +impl Driver for Slider +where + Data::Item: SliderValue, +{ + type Widget = crate::Slider; fn make(&self) -> Self::Widget { crate::Slider::new_with_direction(self.range.clone(), self.step, self.direction) } - fn set(&self, widget: &mut Self::Widget, data: T) -> TkAction { - widget.set_value(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_value(item)) + .unwrap_or(TkAction::EMPTY) } } @@ -296,12 +333,17 @@ impl Spinner { Spinner { range, step } } } -impl Driver for Spinner { - type Widget = crate::Spinner; +impl Driver for Spinner +where + Data::Item: SpinnerValue, +{ + type Widget = crate::Spinner; fn make(&self) -> Self::Widget { crate::Spinner::new(self.range.clone(), self.step) } - fn set(&self, widget: &mut Self::Widget, data: T) -> TkAction { - widget.set_value(data) + fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { + data.get_cloned(key) + .map(|item| widget.set_value(item)) + .unwrap_or(TkAction::EMPTY) } } diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs index 9b7b58844..d6cf3ade7 100644 --- a/crates/kas-widgets/src/view/driver/config.rs +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -7,7 +7,8 @@ use crate::view::driver; use crate::{CheckButton, ComboBox, Spinner}; -use kas::event::config::MousePan; +use kas::event::config::{Config, MousePan}; +use kas::model::{SharedData, SharedRc}; use kas::prelude::*; impl_scope! { @@ -42,7 +43,7 @@ impl_scope! { } } -impl driver::Driver for driver::DefaultView { +impl driver::Driver> for driver::DefaultView { type Widget = EventConfig; fn make(&self) -> Self::Widget { @@ -69,7 +70,8 @@ impl driver::Driver for driver::DefaultView { } } - fn set(&self, widget: &mut Self::Widget, data: kas::event::Config) -> TkAction { + fn set(&self, widget: &mut Self::Widget, data: &SharedRc, key: &()) -> TkAction { + let data = data.get_cloned(key).unwrap(); widget.menu_delay.set_value(data.menu_delay_ms) | widget .touch_select_delay diff --git a/crates/kas-widgets/src/view/list_view.rs b/crates/kas-widgets/src/view/list_view.rs index 39c16bc9f..959d100ff 100644 --- a/crates/kas-widgets/src/view/list_view.rs +++ b/crates/kas-widgets/src/view/list_view.rs @@ -55,7 +55,7 @@ impl_scope! { pub struct ListView< D: Directional, T: ListData, - V: Driver = driver::DefaultView, + V: Driver = driver::DefaultView, > { core: widget_core!(), frame_offset: Offset, @@ -106,7 +106,7 @@ impl_scope! { Self::new_with_dir_driver(D::default(), view, data) } } - impl> ListView { + impl> ListView { /// Set the direction of contents pub fn set_direction(&mut self, direction: Direction) -> TkAction { self.direction = direction; @@ -345,16 +345,19 @@ impl_scope! { let id = self.data.make_id(self.id_ref(), &key); let w = &mut self.widgets[i % solver.cur_len]; if w.key.as_ref() != Some(&key) { - if let Some(item) = self.data.get_cloned(&key) { - w.key = Some(key); - mgr.configure(id, &mut w.widget); - action |= self.view.set(&mut w.widget, item); + // TODO(opt): we only need to configure the widget once + mgr.configure(id, &mut w.widget); + + let act = self.view.set(&mut w.widget, &self.data, &key); + if !act.is_empty() { solve_size_rules( &mut w.widget, mgr.size_mgr(), Some(self.child_size.0), Some(self.child_size.1), ); + w.key = Some(key); + action |= act; } else { w.key = None; // disables drawing and clicking } @@ -552,9 +555,7 @@ impl_scope! { let id = self.data.make_id(self.id_ref(), &key); let mut widget = self.view.make(); mgr.configure(id, &mut widget); - if let Some(item) = self.data.get_cloned(&key) { - *mgr |= self.view.set(&mut widget, item); - } + *mgr |= self.view.set(&mut widget, &self.data, &key); let key = Some(key); self.widgets.push(WidgetData { key, widget }); } diff --git a/crates/kas-widgets/src/view/matrix_view.rs b/crates/kas-widgets/src/view/matrix_view.rs index ddf0ae53a..fcf369767 100644 --- a/crates/kas-widgets/src/view/matrix_view.rs +++ b/crates/kas-widgets/src/view/matrix_view.rs @@ -60,7 +60,7 @@ impl_scope! { #[widget] pub struct MatrixView< T: MatrixData, - V: Driver = driver::DefaultView, + V: Driver = driver::DefaultView, > { core: widget_core!(), frame_offset: Offset, @@ -317,10 +317,11 @@ impl_scope! { let id = self.data.make_id(self.id_ref(), &key); let w = &mut self.widgets[i]; if w.key.as_ref() != Some(&key) { - if let Some(item) = self.data.get_cloned(&key) { + mgr.configure(id, &mut w.widget); + let act = self.view.set(&mut w.widget, &self.data, &key); + if !act.is_empty() { w.key = Some(key); - mgr.configure(id, &mut w.widget); - action |= self.view.set(&mut w.widget, item); + action |= act; solve_size_rules( &mut w.widget, mgr.size_mgr(), @@ -542,9 +543,7 @@ impl_scope! { let id = self.data.make_id(self.id_ref(), &key); let mut widget = self.view.make(); mgr.configure(id, &mut widget); - if let Some(item) = self.data.get_cloned(&key) { - *mgr |= self.view.set(&mut widget, item); - } + *mgr |= self.view.set(&mut widget, &self.data, &key); let key = Some(key); self.widgets.push(WidgetData { key, widget }); } diff --git a/crates/kas-widgets/src/view/single_view.rs b/crates/kas-widgets/src/view/single_view.rs index 5e30cbd8f..48c04eea5 100644 --- a/crates/kas-widgets/src/view/single_view.rs +++ b/crates/kas-widgets/src/view/single_view.rs @@ -21,7 +21,7 @@ impl_scope! { /// or a custom shared data type. /// /// The driver `V` must implement [`Driver`] over data type - /// `::Item`. Several implementations are available in the + /// `::Item`. Several implementations are available in the /// [`driver`] module or a custom implementation may be used. /// /// # Messages @@ -35,7 +35,7 @@ impl_scope! { }] pub struct SingleView< T: SingleData, - V: Driver = driver::DefaultView, + V: Driver = driver::DefaultView, > { core: widget_core!(), view: V, @@ -92,7 +92,7 @@ impl_scope! { /// Set shared data /// /// This method updates the shared data, if supported (see - /// [`SharedData::update`]). Other widgets sharing this data are notified + /// [`SingleData::update`]). Other widgets sharing this data are notified /// of the update, if data is changed. pub fn set_value(&self, mgr: &mut EventMgr, data: T::Item) { self.data.update(mgr, &(), data); @@ -110,8 +110,7 @@ impl_scope! { impl Widget for Self { fn configure(&mut self, mgr: &mut ConfigMgr) { // We set data now, after child is configured - let item = self.data.get_cloned(&()).unwrap(); - *mgr |= self.view.set(&mut self.child, item); + *mgr |= self.view.set(&mut self.child, &self.data, &()); } fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { @@ -119,8 +118,7 @@ impl_scope! { Event::Update { .. } => { let data_ver = self.data.version(); if data_ver > self.data_ver { - let item = self.data.get_cloned(&()).unwrap(); - *mgr |= self.view.set(&mut self.child, item); + *mgr |= self.view.set(&mut self.child, &self.data, &()); self.data_ver = data_ver; } Response::Used diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index 2d0500fef..b02b24f31 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -183,7 +183,7 @@ impl_scope! { struct MyDriver { radio_group: RadioGroup, } -impl Driver<(usize, bool, String)> for MyDriver { +impl Driver<(usize, bool, String), MySharedData> for MyDriver { type Widget = ListEntry; fn make(&self) -> Self::Widget { @@ -196,11 +196,16 @@ impl Driver<(usize, bool, String)> for MyDriver { edit: EditBox::new(String::default()).with_guard(ListEntryGuard), } } - fn set(&self, widget: &mut Self::Widget, data: (usize, bool, String)) -> TkAction { - let label = format!("Entry number {}", data.0 + 1); - widget.label.set_string(label) - | widget.radio.set_bool(data.1) - | widget.edit.set_string(data.2) + + fn set(&self, widget: &mut Self::Widget, data: &MySharedData, key: &usize) -> TkAction { + if let Some(item) = data.get_cloned(key) { + let label = format!("Entry number {}", item.0 + 1); + widget.label.set_string(label) + | widget.radio.set_bool(item.1) + | widget.edit.set_string(item.2) + } else { + TkAction::empty() + } } } From 8b7b65c414d271996190fda94cc2a6ab12394d32 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 10:13:16 +0100 Subject: [PATCH 15/27] examples/data-list-view: key type no longer needs to be part of data --- examples/data-list-view.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index b02b24f31..e8879a2d1 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -87,7 +87,7 @@ impl MySharedData { } impl SharedData for MySharedData { type Key = usize; - type Item = (usize, bool, String); + type Item = (bool, String); fn version(&self) -> u64 { self.data.borrow().ver @@ -102,7 +102,7 @@ impl SharedData for MySharedData { let data = self.data.borrow(); let is_active = data.active == index; let text = data.get(index); - Some((index, is_active, text)) + Some((is_active, text)) } fn update(&self, _: &mut EventMgr, _: &Self::Key, _: Self::Item) {} @@ -183,7 +183,7 @@ impl_scope! { struct MyDriver { radio_group: RadioGroup, } -impl Driver<(usize, bool, String), MySharedData> for MyDriver { +impl Driver<(bool, String), MySharedData> for MyDriver { type Widget = ListEntry; fn make(&self) -> Self::Widget { @@ -199,10 +199,10 @@ impl Driver<(usize, bool, String), MySharedData> for MyDriver { fn set(&self, widget: &mut Self::Widget, data: &MySharedData, key: &usize) -> TkAction { if let Some(item) = data.get_cloned(key) { - let label = format!("Entry number {}", item.0 + 1); + let label = format!("Entry number {}", *key + 1); widget.label.set_string(label) - | widget.radio.set_bool(item.1) - | widget.edit.set_string(item.2) + | widget.radio.set_bool(item.0) + | widget.edit.set_string(item.1) } else { TkAction::empty() } From 9c1f274ae01cfc400ee4521bcb641be4d130662f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 10:27:20 +0100 Subject: [PATCH 16/27] Move SharedData::handle_message to Driver Rationale: the Driver impl knows about the view widget and what messages it sends. The Data type doesn't. --- crates/kas-core/src/model/data_traits.rs | 13 ----- crates/kas-widgets/src/view/driver.rs | 63 ++++++++++++++++++---- crates/kas-widgets/src/view/list_view.rs | 6 +-- crates/kas-widgets/src/view/matrix_view.rs | 6 +-- crates/kas-widgets/src/view/single_view.rs | 6 +-- examples/data-list-view.rs | 34 ++++++------ 6 files changed, 80 insertions(+), 48 deletions(-) diff --git a/crates/kas-core/src/model/data_traits.rs b/crates/kas-core/src/model/data_traits.rs index a234adb26..e336c7896 100644 --- a/crates/kas-core/src/model/data_traits.rs +++ b/crates/kas-core/src/model/data_traits.rs @@ -57,19 +57,6 @@ pub trait SharedData: Debug { /// /// Data types without internal mutability should do nothing. fn update(&self, mgr: &mut EventMgr, key: &Self::Key, item: Self::Item); - - /// Handle a message from a widget - /// - /// This method is called when a view widget returns with a message. - /// It may use [`EventMgr::try_pop_msg`] and update self. - /// - /// The default implementation attempts to extract a value of type - /// [`Self::Item`], passing this to [`Self::update`] on success. - fn handle_message(&self, mgr: &mut EventMgr, key: &Self::Key) { - if let Some(item) = mgr.try_pop_msg() { - self.update(mgr, key, item); - } - } } /// Trait for shared data with access via mutable reference diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index 8d2d8bab1..673b382d7 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -23,12 +23,9 @@ use std::ops::RangeInclusive; /// View widget driver/binder /// -/// The controller binds data items with view widgets. -/// -/// Note that the key type is not made available since in most cases view -/// widgets are not dependent on the element key. In rare cases where the key -/// is needed, it must be added to the data's `Item` type (see `data-list-view` -/// example). +/// This is the controller responsible for building "view" widgets over a given +/// `Data` type, for updating those widgets, and for handling events from those +/// widgets. /// /// Several existing implementations are available, most notably: /// @@ -53,6 +50,16 @@ pub trait Driver>: Debug { /// The widget may expect `configure` to be called at least once before data /// is set and to have `set_rect` called after each time data is set. fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction; + + /// Handle a message from a widget + /// + /// This method is called when a view widget returns with a message. + /// It may use [`EventMgr::try_pop_msg`] and update self. + /// + /// Default implementation: do nothing. + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + let _ = (mgr, data, key); + } } /// Default view widget constructor @@ -116,18 +123,28 @@ impl> Driver for DefaultView { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } impl> Driver for DefaultNav { type Widget = CheckBox; fn make(&self) -> Self::Widget { - CheckBox::new().with_editable(false) + CheckBox::new_on(|mgr, state| mgr.push_msg(state)).with_editable(false) } fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { data.get_cloned(key) .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } /// Custom view widget constructor @@ -161,6 +178,9 @@ where fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { DefaultView.set(widget, data, key) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + DefaultView.handle_message(mgr, data, key); + } }*/ impl> Driver @@ -221,13 +241,18 @@ impl CheckButton { impl> Driver for CheckButton { type Widget = crate::CheckButton; fn make(&self) -> Self::Widget { - crate::CheckButton::new(self.label.clone()).on_toggle(|mgr, state| mgr.push_msg(state)) + crate::CheckButton::new_on(self.label.clone(), |mgr, state| mgr.push_msg(state)) } fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { data.get_cloned(key) .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } /// [`crate::RadioBox`] view widget constructor @@ -244,13 +269,18 @@ impl RadioBox { impl> Driver for RadioBox { type Widget = crate::RadioBox; fn make(&self) -> Self::Widget { - crate::RadioBox::new(self.group.clone()).on_select(|mgr| mgr.push_msg(true)) + crate::RadioBox::new_on(self.group.clone(), |mgr| mgr.push_msg(true)) } fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { data.get_cloned(key) .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } /// [`crate::RadioButton`] view widget constructor @@ -277,6 +307,11 @@ impl> Driver for RadioButton { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } /// [`crate::Slider`] view widget constructor @@ -319,6 +354,11 @@ where .map(|item| widget.set_value(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } /// [`crate::Spinner`] view widget constructor @@ -346,4 +386,9 @@ where .map(|item| widget.set_value(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(state) = mgr.try_pop_msg() { + data.update(mgr, key, state); + } + } } diff --git a/crates/kas-widgets/src/view/list_view.rs b/crates/kas-widgets/src/view/list_view.rs index 959d100ff..3629a09d0 100644 --- a/crates/kas-widgets/src/view/list_view.rs +++ b/crates/kas-widgets/src/view/list_view.rs @@ -44,8 +44,8 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`SharedData::handle_message`] method is - /// called. After calling [`SharedData::handle_message`], this widget attempts to + /// When a child pushes a message, the [`Driver::handle_message`] method is + /// called. After calling [`Driver::handle_message`], this widget attempts to /// read and handle [`SelectMsg`]. /// /// When selection is enabled and an item is selected or deselected, this @@ -752,7 +752,7 @@ impl_scope! { None => return, }; - self.data.handle_message(mgr, &key); + self.view.handle_message(mgr, &self.data, &key); if let Some(SelectMsg) = mgr.try_pop_msg() { match self.sel_mode { diff --git a/crates/kas-widgets/src/view/matrix_view.rs b/crates/kas-widgets/src/view/matrix_view.rs index fcf369767..e5423d082 100644 --- a/crates/kas-widgets/src/view/matrix_view.rs +++ b/crates/kas-widgets/src/view/matrix_view.rs @@ -50,8 +50,8 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`SharedData::handle_message`] method is - /// called. After calling [`SharedData::handle_message`], this widget attempts + /// When a child pushes a message, the [`Driver::handle_message`] method is + /// called. After calling [`Driver::handle_message`], this widget attempts /// to read and handle [`SelectMsg`]. /// /// When selection is enabled and an item is selected or deselected, this @@ -766,7 +766,7 @@ impl_scope! { None => return, }; - self.data.handle_message(mgr, &key); + self.view.handle_message(mgr, &self.data, &key); if let Some(SelectMsg) = mgr.try_pop_msg() { match self.sel_mode { diff --git a/crates/kas-widgets/src/view/single_view.rs b/crates/kas-widgets/src/view/single_view.rs index 48c04eea5..a503dc313 100644 --- a/crates/kas-widgets/src/view/single_view.rs +++ b/crates/kas-widgets/src/view/single_view.rs @@ -26,7 +26,7 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`SharedData::handle_message`] method is + /// When a child pushes a message, the [`Driver::handle_message`] method is /// called. #[autoimpl(Debug ignore self.view)] #[derive(Clone)] @@ -92,7 +92,7 @@ impl_scope! { /// Set shared data /// /// This method updates the shared data, if supported (see - /// [`SingleData::update`]). Other widgets sharing this data are notified + /// [`SharedData::update`]). Other widgets sharing this data are notified /// of the update, if data is changed. pub fn set_value(&self, mgr: &mut EventMgr, data: T::Item) { self.data.update(mgr, &(), data); @@ -128,7 +128,7 @@ impl_scope! { } fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - self.data.handle_message(mgr, &()); + self.view.handle_message(mgr, &self.data, &()); } } } diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index e8879a2d1..4ef7cd00d 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -106,23 +106,6 @@ impl SharedData for MySharedData { } fn update(&self, _: &mut EventMgr, _: &Self::Key, _: Self::Item) {} - - fn handle_message(&self, mgr: &mut EventMgr, key: &Self::Key) { - if let Some(msg) = mgr.try_pop_msg() { - let mut data = self.data.borrow_mut(); - data.ver += 1; - match msg { - EntryMsg::Select => { - data.active = *key; - } - EntryMsg::Update(text) => { - data.strings.insert(*key, text.clone()); - } - } - mgr.push_msg(Control::Update(data.get(data.active))); - mgr.update_all(self.id, 0); - } - } } impl ListData for MySharedData { fn len(&self) -> usize { @@ -207,6 +190,23 @@ impl Driver<(bool, String), MySharedData> for MyDriver { TkAction::empty() } } + + fn handle_message(&self, mgr: &mut EventMgr, data: &MySharedData, key: &usize) { + if let Some(msg) = mgr.try_pop_msg() { + let mut borrow = data.data.borrow_mut(); + borrow.ver += 1; + match msg { + EntryMsg::Select => { + borrow.active = *key; + } + EntryMsg::Update(text) => { + borrow.strings.insert(*key, text.clone()); + } + } + mgr.push_msg(Control::Update(borrow.get(borrow.active))); + mgr.update_all(data.id, 0); + } + } } fn main() -> kas::shell::Result<()> { From 5bc7eeebe410ed49a2465c0ac91ce72055735247 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 10:37:27 +0100 Subject: [PATCH 17/27] =?UTF-8?q?Rename=20kas=5Fwidgets::edit=5Ffield=20?= =?UTF-8?q?=E2=86=92=20edit=20and=20make=20a=20public=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also: add NotifyGuard type Also: event handling for Driver impls over Edit* widgets --- .../src/{edit_field.rs => edit.rs} | 73 ++++++++++++------- crates/kas-widgets/src/lib.rs | 4 +- crates/kas-widgets/src/view/driver.rs | 34 +++++---- 3 files changed, 67 insertions(+), 44 deletions(-) rename crates/kas-widgets/src/{edit_field.rs => edit.rs} (95%) diff --git a/crates/kas-widgets/src/edit_field.rs b/crates/kas-widgets/src/edit.rs similarity index 95% rename from crates/kas-widgets/src/edit_field.rs rename to crates/kas-widgets/src/edit.rs index 97e4a4bc3..7e50dbe8b 100644 --- a/crates/kas-widgets/src/edit_field.rs +++ b/crates/kas-widgets/src/edit.rs @@ -3,7 +3,7 @@ // You may obtain a copy of the License in the LICENSE-APACHE file or at: // https://www.apache.org/licenses/LICENSE-2.0 -//! Text-edit field +//! The [`EditField`] and [`EditBox`] widgets, plus supporting items use kas::event::components::{TextInput, TextInputAction}; use kas::event::{Command, CursorIcon, Scroll, ScrollDelta}; @@ -48,7 +48,15 @@ enum EditAction { /// /// All methods have a default implementation which does nothing. /// -/// This trait is implemented for `()` (does nothing). +/// Pre-built implementations: +/// +/// - `()`: does nothing +/// - `GuardNotify`: clones text to a `String` and pushes as a message ([`EventMgr::push_msg`]) +/// on `activate` and `focus_lost` events +/// - `GuardActivate: calls a closure on `activate` +/// - `GuardAFL`: calls a closure on `activate` and `focus_lost` +/// - `GuardEdit`: calls a closure on `edit` +/// - `GuardUpdate`: calls a closure on `update` pub trait EditGuard: Debug + Sized + 'static { /// Activation guard /// @@ -98,11 +106,24 @@ pub trait EditGuard: Debug + Sized + 'static { impl EditGuard for () {} +#[derive(Debug, Eq, PartialEq, Hash)] +pub struct GuardNotify; +impl EditGuard for GuardNotify { + fn activate(edit: &mut EditField, mgr: &mut EventMgr) { + mgr.push_msg(edit.get_string()); + } + + #[inline] + fn focus_lost(edit: &mut EditField, mgr: &mut EventMgr) { + Self::activate(edit, mgr); + } +} + /// An [`EditGuard`] impl which calls a closure when activated #[autoimpl(Debug ignore self.0)] #[derive(Clone)] -pub struct EditActivate(pub F); -impl EditGuard for EditActivate +pub struct GuardActivate(pub F); +impl EditGuard for GuardActivate where F: FnMut(&str, &mut EventMgr) + 'static, { @@ -114,8 +135,8 @@ where /// An [`EditGuard`] impl which calls a closure when activated or focus is lost #[autoimpl(Debug ignore self.0)] #[derive(Clone)] -pub struct EditAFL(pub F); -impl EditGuard for EditAFL +pub struct GuardAFL(pub F); +impl EditGuard for GuardAFL where F: FnMut(&str, &mut EventMgr) + 'static, { @@ -130,8 +151,8 @@ where /// An [`EditGuard`] impl which calls a closure when edited #[autoimpl(Debug ignore self.0)] #[derive(Clone)] -pub struct EditEdit(pub F); -impl EditGuard for EditEdit +pub struct GuardEdit(pub F); +impl EditGuard for GuardEdit where F: FnMut(&str, &mut EventMgr) + 'static, { @@ -143,8 +164,8 @@ where /// An [`EditGuard`] impl which calls a closure when updated #[autoimpl(Debug ignore self.0)] #[derive(Clone)] -pub struct EditUpdate(pub F); -impl EditGuard for EditUpdate { +pub struct GuardUpdate(pub F); +impl EditGuard for GuardUpdate { fn update(edit: &mut EditField) { (edit.guard.0)(edit.text.text()); } @@ -213,11 +234,11 @@ impl EditBox<()> { /// This method is a parametisation of [`EditBox::with_guard`]. Any guard /// previously assigned to the `EditBox` will be replaced. #[must_use] - pub fn on_activate(self, f: F) -> EditBox> + pub fn on_activate(self, f: F) -> EditBox> where F: FnMut(&str, &mut EventMgr) + 'static, { - self.with_guard(EditActivate(f)) + self.with_guard(GuardActivate(f)) } /// Set a guard function, called on activation and input-focus lost @@ -228,11 +249,11 @@ impl EditBox<()> { /// This method is a parametisation of [`EditBox::with_guard`]. Any guard /// previously assigned to the `EditBox` will be replaced. #[must_use] - pub fn on_afl(self, f: F) -> EditBox> + pub fn on_afl(self, f: F) -> EditBox> where F: FnMut(&str, &mut EventMgr) + 'static, { - self.with_guard(EditAFL(f)) + self.with_guard(GuardAFL(f)) } /// Set a guard function, called on edit @@ -242,11 +263,11 @@ impl EditBox<()> { /// This method is a parametisation of [`EditBox::with_guard`]. Any guard /// previously assigned to the `EditBox` will be replaced. #[must_use] - pub fn on_edit(self, f: F) -> EditBox> + pub fn on_edit(self, f: F) -> EditBox> where F: FnMut(&str, &mut EventMgr) + 'static, { - self.with_guard(EditEdit(f)) + self.with_guard(GuardEdit(f)) } /// Set a guard function, called on update @@ -257,8 +278,8 @@ impl EditBox<()> { /// This method is a parametisation of [`EditBox::with_guard`]. Any guard /// previously assigned to the `EditBox` will be replaced. #[must_use] - pub fn on_update(self, f: F) -> EditBox> { - self.with_guard(EditUpdate(f)) + pub fn on_update(self, f: F) -> EditBox> { + self.with_guard(GuardUpdate(f)) } } @@ -618,8 +639,8 @@ impl EditField<()> { pub fn on_activate( self, f: F, - ) -> EditField> { - self.with_guard(EditActivate(f)) + ) -> EditField> { + self.with_guard(GuardActivate(f)) } /// Set a guard function, called on activation and input-focus lost @@ -630,8 +651,8 @@ impl EditField<()> { /// This method is a parametisation of [`EditField::with_guard`]. Any guard /// previously assigned to the `EditField` will be replaced. #[must_use] - pub fn on_afl(self, f: F) -> EditField> { - self.with_guard(EditAFL(f)) + pub fn on_afl(self, f: F) -> EditField> { + self.with_guard(GuardAFL(f)) } /// Set a guard function, called on edit @@ -641,8 +662,8 @@ impl EditField<()> { /// This method is a parametisation of [`EditField::with_guard`]. Any guard /// previously assigned to the `EditField` will be replaced. #[must_use] - pub fn on_edit(self, f: F) -> EditField> { - self.with_guard(EditEdit(f)) + pub fn on_edit(self, f: F) -> EditField> { + self.with_guard(GuardEdit(f)) } /// Set a guard function, called on update @@ -653,8 +674,8 @@ impl EditField<()> { /// This method is a parametisation of [`EditField::with_guard`]. Any guard /// previously assigned to the `EditField` will be replaced. #[must_use] - pub fn on_update(self, f: F) -> EditField> { - self.with_guard(EditUpdate(f)) + pub fn on_update(self, f: F) -> EditField> { + self.with_guard(GuardUpdate(f)) } } diff --git a/crates/kas-widgets/src/lib.rs b/crates/kas-widgets/src/lib.rs index ee5dc2b37..1b0b81325 100644 --- a/crates/kas-widgets/src/lib.rs +++ b/crates/kas-widgets/src/lib.rs @@ -63,7 +63,7 @@ mod check_box; mod combobox; pub mod dialog; mod drag; -mod edit_field; +pub mod edit; mod filler; mod frame; mod grid; @@ -93,7 +93,7 @@ pub use button::{Button, TextButton}; pub use check_box::{CheckBox, CheckButton}; pub use combobox::ComboBox; pub use drag::DragHandle; -pub use edit_field::{EditBox, EditField, EditGuard}; +pub use edit::{EditBox, EditField, EditGuard}; pub use filler::Filler; pub use frame::{Frame, PopupFrame}; pub use grid::{BoxGrid, Grid}; diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index 673b382d7..b3c58334b 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -10,10 +10,8 @@ mod config; -use crate::{ - CheckBox, EditBox, EditField, EditGuard, Label, NavFrame, ProgressBar, RadioGroup, SliderValue, - SpinnerValue, -}; +use crate::edit::{EditBox, EditField, GuardNotify}; +use crate::{CheckBox, Label, NavFrame, ProgressBar, RadioGroup, SliderValue, SpinnerValue}; use kas::model::SharedData; use kas::prelude::*; use std::default::Default; @@ -183,33 +181,37 @@ where } }*/ -impl> Driver - for Widget> -{ - type Widget = EditField; +impl> Driver for Widget> { + type Widget = EditField; fn make(&self) -> Self::Widget { - let guard = G::default(); - EditField::new("".to_string()).with_guard(guard) + EditField::new("".to_string()).with_guard(GuardNotify) } fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { data.get_cloned(key) .map(|item| widget.set_string(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(item) = mgr.try_pop_msg() { + data.update(mgr, key, item); + } + } } -impl> Driver - for Widget> -{ - type Widget = EditBox; +impl> Driver for Widget> { + type Widget = EditBox; fn make(&self) -> Self::Widget { - let guard = G::default(); - EditBox::new("".to_string()).with_guard(guard) + EditBox::new("".to_string()).with_guard(GuardNotify) } fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { data.get_cloned(key) .map(|item| widget.set_string(item)) .unwrap_or(TkAction::EMPTY) } + fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + if let Some(item) = mgr.try_pop_msg() { + data.update(mgr, key, item); + } + } } impl> Driver From bd39c9aaf542b91f31e9b3ee7e45293ecfbff0be Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 11:33:40 +0100 Subject: [PATCH 18/27] Slider, Spinner: add optional event handling closures Also: add EventMgr::try_observe_msg --- crates/kas-core/src/event/manager.rs | 4 ++ crates/kas-core/src/event/manager/mgr_pub.rs | 5 ++ crates/kas-widgets/src/slider.rs | 48 ++++++++++++---- crates/kas-widgets/src/spinner.rs | 60 ++++++++++++++++---- examples/gallery.rs | 18 +++--- examples/mandlebrot/mandlebrot.rs | 4 +- 6 files changed, 107 insertions(+), 32 deletions(-) diff --git a/crates/kas-core/src/event/manager.rs b/crates/kas-core/src/event/manager.rs index 7c6535c4d..d2f339b13 100644 --- a/crates/kas-core/src/event/manager.rs +++ b/crates/kas-core/src/event/manager.rs @@ -384,6 +384,10 @@ impl Message { fn downcast(self) -> Result, Box> { self.any.downcast::() } + + fn downcast_ref(&self) -> Option<&T> { + self.any.downcast_ref::() + } } /// Manager of event-handling and toolkit actions diff --git a/crates/kas-core/src/event/manager/mgr_pub.rs b/crates/kas-core/src/event/manager/mgr_pub.rs index ec9e63f05..512af3439 100644 --- a/crates/kas-core/src/event/manager/mgr_pub.rs +++ b/crates/kas-core/src/event/manager/mgr_pub.rs @@ -508,6 +508,11 @@ impl<'a> EventMgr<'a> { } } + /// Try observing the last message on the stack without popping + pub fn try_observe_msg(&self) -> Option<&M> { + self.messages.last().and_then(|m| m.downcast_ref::()) + } + /// Set a scroll action /// /// When setting [`Scroll::Rect`], use the widgets own coordinate space. diff --git a/crates/kas-widgets/src/slider.rs b/crates/kas-widgets/src/slider.rs index aab2f9c32..e73797eff 100644 --- a/crates/kas-widgets/src/slider.rs +++ b/crates/kas-widgets/src/slider.rs @@ -7,6 +7,7 @@ use std::fmt::Debug; use std::ops::{Add, RangeInclusive, Sub}; +use std::rc::Rc; use std::time::Duration; use super::DragHandle; @@ -89,11 +90,8 @@ impl_scope! { /// A slider /// /// Sliders allow user input of a value from a fixed range. - /// - /// # Messages - /// - /// On value change, pushes a value of type `T`. - #[derive(Clone, Debug)] + #[autoimpl(Debug ignore self.on_move)] + #[derive(Clone)] #[widget{ key_nav = true; hover_highlight = true; @@ -107,6 +105,7 @@ impl_scope! { value: T, #[widget] handle: DragHandle, + on_move: Option>, } impl Self where D: Default { @@ -122,6 +121,17 @@ impl_scope! { pub fn new(range: RangeInclusive, step: T) -> Self { Slider::new_with_direction(range, step, D::default()) } + + /// Construct a spinner with event handler `f` + /// + /// This closure is called when the slider is moved. + #[inline] + pub fn new_on(range: RangeInclusive, step: T, f: F) -> Self + where + F: Fn(&mut EventMgr, T) + 'static, + { + Slider::new(range, step).on_move(f) + } } impl Self { @@ -144,9 +154,23 @@ impl_scope! { step, value, handle: DragHandle::new(), + on_move: None, } } + /// Set event handler `f` + /// + /// This closure is called when the slider is moved. + #[inline] + #[must_use] + pub fn on_move(mut self, f: F) -> Self + where + F: Fn(&mut EventMgr, T) + 'static, + { + self.on_move = Some(Rc::new(f)); + self + } + /// Get the slider's direction #[inline] pub fn direction(&self) -> Direction { @@ -208,7 +232,7 @@ impl_scope! { } } - fn set_offset_and_push_msg(&mut self, mgr: &mut EventMgr, offset: Offset) { + fn set_offset_and_emit(&mut self, mgr: &mut EventMgr, offset: Offset) { let b = *self.range.end() - *self.range.start(); let max_offset = self.handle.max_offset(); let mut a = match self.direction.is_vertical() { @@ -222,7 +246,9 @@ impl_scope! { if value != self.value { self.value = value; *mgr |= self.handle.set_offset(self.offset()).1; - mgr.push_msg(self.value); + if let Some(ref f) = self.on_move { + f(mgr, value); + } } } } @@ -294,12 +320,14 @@ impl_scope! { let action = self.set_value(v); if !action.is_empty() { mgr.send_action(action); - mgr.push_msg(self.value); + if let Some(ref f) = self.on_move { + f(mgr, self.value); + } } } Event::PressStart { source, coord, .. } => { let offset = self.handle.handle_press_on_track(mgr, source, coord); - self.set_offset_and_push_msg(mgr, offset); + self.set_offset_and_emit(mgr, offset); } _ => return Response::Unused, } @@ -310,7 +338,7 @@ impl_scope! { if let Some(MsgPressFocus) = mgr.try_pop_msg() { mgr.set_nav_focus(self.id(), false); } else if let Some(offset) = mgr.try_pop_msg() { - self.set_offset_and_push_msg(mgr, offset); + self.set_offset_and_emit(mgr, offset); } } } diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index c2d726597..ff317384f 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -11,6 +11,7 @@ use kas::prelude::*; use kas::theme::{Background, FrameStyle, MarkStyle}; use std::cmp::Ord; use std::ops::RangeInclusive; +use std::rc::Rc; /// Requirements on type used by [`Spinner`] /// @@ -79,6 +80,9 @@ enum SpinBtn { Up, } +#[derive(Debug)] +struct ValueMsg(T); + #[derive(Clone, Debug)] struct SpinnerGuard { value: T, @@ -111,7 +115,7 @@ impl EditGuard for SpinnerGuard { *mgr |= edit.set_string(edit.guard.value.to_string()); edit.set_error_state(false); } - mgr.push_msg(edit.guard.value); + mgr.push_msg(ValueMsg(edit.guard.value)); } fn focus_lost(edit: &mut EditField, mgr: &mut EventMgr) { @@ -126,7 +130,7 @@ impl EditGuard for SpinnerGuard { Ok(value) if edit.guard.range().contains(&value) => { if value != edit.guard.value { edit.guard.value = value; - mgr.push_msg(value); + mgr.push_msg(ValueMsg(value)); } false } @@ -151,14 +155,7 @@ impl_scope! { /// - Ensure that range end points are a multiple of `step` /// - With floating-point types, ensure that `step` is exactly /// representable, e.g. an integer or a power of 2. - /// - /// Sends a message of type `T` when changed, specifically: - /// - /// - If the increment/decrement buttons, Up/Down - /// keys or mouse scroll wheel is used and the value changes - /// - If the value is adjusted via the edit box and the result is valid - /// - If Enter is pressed in the edit box - #[derive(Clone, Debug)] + #[autoimpl(Debug ignore self.on_change)] #[widget { layout = frame(FrameStyle::EditBox): row: [ self.edit, @@ -177,10 +174,12 @@ impl_scope! { #[widget] b_down: MarkButton, step: T, + on_change: Option>, } impl Self { - /// Construct with given `range` and `step` + /// Construct a spinner with given `range` and `step` + #[inline] pub fn new(range: RangeInclusive, step: T) -> Self { Spinner { core: Default::default(), @@ -188,9 +187,39 @@ impl_scope! { b_up: MarkButton::new(MarkStyle::Point(Direction::Up), SpinBtn::Up), b_down: MarkButton::new(MarkStyle::Point(Direction::Down), SpinBtn::Down), step: step, + on_change: None, } } + /// Construct a spinner with event handler `f` + /// + /// This closure is called when the value is changed. + #[inline] + pub fn new_on(range: RangeInclusive, step: T, f: F) -> Self + where + F: Fn(&mut EventMgr, T) + 'static, + { + Spinner::new(range, step).on_change(f) + } + + /// Set event handler `f` + /// + /// This closure is called when the value is changed, specifically: + /// + /// - If the increment/decrement buttons, Up/Down + /// keys or mouse scroll wheel is used and the value changes + /// - If the value is adjusted via the edit box and the result is valid + /// - If Enter is pressed in the edit box + #[inline] + #[must_use] + pub fn on_change(mut self, f: F) -> Self + where + F: Fn(&mut EventMgr, T) + 'static, + { + self.on_change = Some(Rc::new(f)); + self + } + /// Set the initial value #[inline] #[must_use] @@ -227,7 +256,9 @@ impl_scope! { if value != self.edit.guard.value { self.edit.guard.value = value; - mgr.push_msg(value); + if let Some(ref f) = self.on_change { + f(mgr, value); + } } self.edit.set_error_state(false); @@ -280,6 +311,11 @@ impl_scope! { } fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { + if let Some(ValueMsg(value)) = mgr.try_pop_msg() { + if let Some(ref f) = self.on_change { + f(mgr, value); + } + } if let Some(btn) = mgr.try_pop_msg::() { self.handle_btn(mgr, btn); } diff --git a/examples/gallery.rs b/examples/gallery.rs index c05c3548b..1fbb3b503 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -160,8 +160,10 @@ fn widgets() -> Box { MenuEntry::new("T&wo", Item::Combo(2)), MenuEntry::new("Th&ree", Item::Combo(3)), ]), - #[widget] spin: Spinner = Spinner::new(0..=10, 1), - #[widget] sd: Slider = Slider::new(0..=10, 1), + #[widget] spin: Spinner = Spinner::new(0..=10, 1) + .on_change(|mgr, value| mgr.push_msg(Item::Spinner(value))), + #[widget] sd: Slider = Slider::new(0..=10, 1) + .on_move(|mgr, value| mgr.push_msg(Item::Slider(value))), #[widget] sc: ScrollBar = ScrollBar::new().with_limits(100, 20), #[widget] pg: ProgressBar = ProgressBar::new(), #[widget] sv = img_rustacean.with_scaling(|s| { @@ -174,17 +176,15 @@ fn widgets() -> Box { impl Widget for Self { fn handle_message(&mut self, mgr: &mut EventMgr, index: usize) { if let Some(msg) = mgr.try_pop_msg::() { - if index == widget_index![self.spin] { - *mgr |= self.sd.set_value(msg); - mgr.push_msg(Item::Spinner(msg)); - } else if index == widget_index![self.sd] { - *mgr |= self.spin.set_value(msg); - mgr.push_msg(Item::Slider(msg)); - } else if index == widget_index![self.sc] { + if index == widget_index![self.sc] { let ratio = msg as f32 / self.sc.max_value() as f32; *mgr |= self.pg.set_value(ratio); mgr.push_msg(Item::Scroll(msg)) } + } else if let Some(Item::Spinner(value)) = mgr.try_observe_msg() { + *mgr |= self.sd.set_value(*value); + } else if let Some(Item::Slider(value)) = mgr.try_observe_msg() { + *mgr |= self.spin.set_value(*value); } } } diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs index 579b48ade..69ccb7fbe 100644 --- a/examples/mandlebrot/mandlebrot.rs +++ b/examples/mandlebrot/mandlebrot.rs @@ -446,7 +446,9 @@ impl_scope! { impl MandlebrotWindow { fn new() -> MandlebrotWindow { - let slider = Slider::new(0..=256, 1).with_value(64); + let slider = Slider::new(0..=256, 1) + .with_value(64) + .on_move(|mgr, iter| mgr.push_msg(iter)); let mbrot = Mandlebrot::new(); MandlebrotWindow { core: Default::default(), From b605ba162aaccf96a53d7cc2a8a50e334657b61d Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 12:47:17 +0100 Subject: [PATCH 19/27] driver/config: Add handle_message --- crates/kas-core/src/model/shared_rc.rs | 27 ++++++++- crates/kas-widgets/src/view/driver/config.rs | 63 ++++++++++++++++---- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/crates/kas-core/src/model/shared_rc.rs b/crates/kas-core/src/model/shared_rc.rs index d44630953..ff43c0c48 100644 --- a/crates/kas-core/src/model/shared_rc.rs +++ b/crates/kas-core/src/model/shared_rc.rs @@ -13,9 +13,9 @@ use crate::event::EventMgr; use crate::event::UpdateId; use crate::model::*; -use std::cell::{BorrowError, Ref, RefCell}; +use std::cell::{BorrowError, Ref, RefCell, RefMut}; use std::fmt::Debug; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::rc::Rc; /// Wrapper for single-thread shared data @@ -33,6 +33,7 @@ use std::rc::Rc; #[derive(Clone, Debug, Default)] pub struct SharedRc(Rc<(UpdateId, RefCell<(T, u64)>)>); +/// A borrowed reference pub struct SharedRcRef<'a, T>(Ref<'a, (T, u64)>); impl<'a, T> Deref for SharedRcRef<'a, T> { type Target = T; @@ -41,6 +42,20 @@ impl<'a, T> Deref for SharedRcRef<'a, T> { } } +/// A mutably borrowed reference +pub struct SharedRcRefMut<'a, T>(RefMut<'a, (T, u64)>); +impl<'a, T> Deref for SharedRcRefMut<'a, T> { + type Target = T; + fn deref(&self) -> &T { + &self.0.deref().0 + } +} +impl<'a, T> DerefMut for SharedRcRefMut<'a, T> { + fn deref_mut(&mut self) -> &mut T { + &mut self.0.deref_mut().0 + } +} + impl SharedRc { /// Construct with given data pub fn new(data: T) -> Self { @@ -80,6 +95,14 @@ impl SharedRc { pub fn try_borrow(&self) -> Result, BorrowError> { (self.0).1.try_borrow().map(SharedRcRef) } + + /// Mutably borrows the wrapped value, notifying other users of an update. + pub fn update_mut(&self, mgr: &mut EventMgr) -> SharedRcRefMut { + mgr.update_all((self.0).0, 0); + let mut cell = (self.0).1.borrow_mut(); + cell.1 += 1; + SharedRcRefMut(cell) + } } impl SharedData for SharedRc { diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs index d6cf3ade7..8031d6410 100644 --- a/crates/kas-widgets/src/view/driver/config.rs +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -11,6 +11,20 @@ use kas::event::config::{Config, MousePan}; use kas::model::{SharedData, SharedRc}; use kas::prelude::*; +#[derive(Debug)] +enum Msg { + MenuDelay(u32), + TouchSelectDelay(u32), + ScrollFlickTimeout(u32), + ScrollFlickMul(f32), + ScrollFlickSub(f32), + PanDistThresh(f32), + MousePan(MousePan), + MouseTextPan(MousePan), + MouseNavFocus(bool), + TouchNavFocus(bool), +} + impl_scope! { /// A widget for viewing event config #[widget{ @@ -53,20 +67,29 @@ impl driver::Driver> for driver::DefaultView { ("With Ctrl key", MousePan::WithCtrl), ("Always", MousePan::Always), ]); - let mouse_text_pan = mouse_pan.clone(); EventConfig { core: Default::default(), - menu_delay: Spinner::new(0..=10_000, 50), - touch_select_delay: Spinner::new(0..=10_000, 50), - scroll_flick_timeout: Spinner::new(0..=1_000, 5), - scroll_flick_mul: Spinner::new(0.0..=1.0, 0.0625), - scroll_flick_sub: Spinner::new(0.0..=1.0e4, 10.0), - pan_dist_thresh: Spinner::new(0.125..=10.0, 0.125), - mouse_pan, - mouse_text_pan, - mouse_nav_focus: CheckButton::new("Mouse navigation focus"), - touch_nav_focus: CheckButton::new("Touchscreen navigation focus"), + menu_delay: Spinner::new(0..=10_000, 50) + .on_change(|mgr, v| mgr.push_msg(Msg::MenuDelay(v))), + touch_select_delay: Spinner::new(0..=10_000, 50) + .on_change(|mgr, v| mgr.push_msg(Msg::TouchSelectDelay(v))), + scroll_flick_timeout: Spinner::new(0..=1_000, 5) + .on_change(|mgr, v| mgr.push_msg(Msg::ScrollFlickTimeout(v))), + scroll_flick_mul: Spinner::new(0.0..=1.0, 0.0625) + .on_change(|mgr, v| mgr.push_msg(Msg::ScrollFlickMul(v))), + scroll_flick_sub: Spinner::new(0.0..=1.0e4, 10.0) + .on_change(|mgr, v| mgr.push_msg(Msg::ScrollFlickSub(v))), + pan_dist_thresh: Spinner::new(0.125..=10.0, 0.125) + .on_change(|mgr, v| mgr.push_msg(Msg::PanDistThresh(v))), + mouse_pan: mouse_pan + .clone() + .on_select(|mgr, v| mgr.push_msg(Msg::MousePan(v))), + mouse_text_pan: mouse_pan.on_select(|mgr, v| mgr.push_msg(Msg::MouseTextPan(v))), + mouse_nav_focus: CheckButton::new("Mouse navigation focus") + .on_toggle(|mgr, v| mgr.push_msg(Msg::MouseNavFocus(v))), + touch_nav_focus: CheckButton::new("Touchscreen navigation focus") + .on_toggle(|mgr, v| mgr.push_msg(Msg::TouchNavFocus(v))), } } @@ -89,4 +112,22 @@ impl driver::Driver> for driver::DefaultView { | widget.mouse_nav_focus.set_bool(data.mouse_nav_focus) | widget.touch_nav_focus.set_bool(data.touch_nav_focus) } + + fn handle_message(&self, mgr: &mut EventMgr, data: &SharedRc, _: &()) { + if let Some(msg) = mgr.try_pop_msg() { + let mut data = data.update_mut(mgr); + match msg { + Msg::MenuDelay(v) => data.menu_delay_ms = v, + Msg::TouchSelectDelay(v) => data.touch_select_delay_ms = v, + Msg::ScrollFlickTimeout(v) => data.scroll_flick_timeout_ms = v, + Msg::ScrollFlickMul(v) => data.scroll_flick_mul = v, + Msg::ScrollFlickSub(v) => data.scroll_flick_sub = v, + Msg::PanDistThresh(v) => data.pan_dist_thresh = v, + Msg::MousePan(v) => data.mouse_pan = v, + Msg::MouseTextPan(v) => data.mouse_text_pan = v, + Msg::MouseNavFocus(v) => data.mouse_nav_focus = v, + Msg::TouchNavFocus(v) => data.touch_nav_focus = v, + } + } + } } From 2ac95ba02dcb7318bfc9a81659dfa0b2763721e2 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 12:56:38 +0100 Subject: [PATCH 20/27] driver/config: add Reset button --- crates/kas-widgets/src/view/driver/config.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs index 8031d6410..e85f812d7 100644 --- a/crates/kas-widgets/src/view/driver/config.rs +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -6,12 +6,12 @@ //! Drivers for configuration types use crate::view::driver; -use crate::{CheckButton, ComboBox, Spinner}; +use crate::{CheckButton, ComboBox, Spinner, TextButton}; use kas::event::config::{Config, MousePan}; use kas::model::{SharedData, SharedRc}; use kas::prelude::*; -#[derive(Debug)] +#[derive(Clone, Debug)] enum Msg { MenuDelay(u32), TouchSelectDelay(u32), @@ -23,6 +23,7 @@ enum Msg { MouseTextPan(MousePan), MouseNavFocus(bool), TouchNavFocus(bool), + Reset, } impl_scope! { @@ -39,6 +40,7 @@ impl_scope! { 0, 7: "Mouse text pan:"; 1..3, 7: self.mouse_text_pan; 1..3, 8: self.mouse_nav_focus; 1..3, 9: self.touch_nav_focus; + 0, 10: "Restore default values:"; 1..3, 10: TextButton::new_msg("Reset", Msg::Reset); }; }] #[derive(Debug)] @@ -127,6 +129,7 @@ impl driver::Driver> for driver::DefaultView { Msg::MouseTextPan(v) => data.mouse_text_pan = v, Msg::MouseNavFocus(v) => data.mouse_nav_focus = v, Msg::TouchNavFocus(v) => data.touch_nav_focus = v, + Msg::Reset => *data = Config::default(), } } } From cf9a69ae21d9ed97f7db3bd501e17deee3afc394 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 16:03:35 +0100 Subject: [PATCH 21/27] =?UTF-8?q?Rename=20Driver::handle=5Fmessage=20?= =?UTF-8?q?=E2=86=92=20on=5Fmessage;=20add=20widget=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/kas-widgets/src/view/driver.rs | 51 ++++++++++++++------ crates/kas-widgets/src/view/driver/config.rs | 8 ++- crates/kas-widgets/src/view/list_view.rs | 9 ++-- crates/kas-widgets/src/view/matrix_view.rs | 9 ++-- crates/kas-widgets/src/view/single_view.rs | 4 +- examples/data-list-view.rs | 8 ++- 6 files changed, 62 insertions(+), 27 deletions(-) diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index b3c58334b..7b04a4eca 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -51,12 +51,33 @@ pub trait Driver>: Debug { /// Handle a message from a widget /// - /// This method is called when a view widget returns with a message. - /// It may use [`EventMgr::try_pop_msg`] and update self. + /// This method is called when a view widget returns with a message; it + /// may retrieve this message with [`EventMgr::try_pop_msg`]. + /// + /// There are three main ways of implementing this method: + /// + /// 1. Do nothing. This is always safe, though may result in unhandled + /// message warnings when the view widget is interactive. + /// 2. On user input actions, view widgets send a message including their + /// content (potentially wrapped with a user-defined enum or struct + /// type). The implementation of this method retrieves this message and + /// updates `data` given this content. In this case, the `widget` + /// parameter is not used. + /// 3. On user input actions, view widgets send a "trigger" message (likely + /// a unit struct). The implementation of this method retrieves this + /// message updates `data` using values read from `widget`. + /// + /// For examples, see implementations of [`DefaultView`]. /// /// Default implementation: do nothing. - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { - let _ = (mgr, data, key); + fn on_message( + &self, + mgr: &mut EventMgr, + widget: &mut Self::Widget, + data: &Data, + key: &Data::Key, + ) { + let _ = (mgr, widget, data, key); } } @@ -121,7 +142,7 @@ impl> Driver for DefaultView { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } @@ -138,7 +159,7 @@ impl> Driver for DefaultNav { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } @@ -176,8 +197,8 @@ where fn set(&self, widget: &mut Self::Widget, data: &Data, key: &Data::Key) -> TkAction { DefaultView.set(widget, data, key) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { - DefaultView.handle_message(mgr, data, key); + fn on_message(&self, mgr: &mut EventMgr, widget: &mut Self::Widget, data: &Data, key: &Data::Key) { + DefaultView.on_message(mgr, widget, data, key); } }*/ @@ -191,7 +212,7 @@ impl> Driver for Widget> Driver for Widget> Driver for CheckButton { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } @@ -278,7 +299,7 @@ impl> Driver for RadioBox { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } @@ -309,7 +330,7 @@ impl> Driver for RadioButton { .map(|item| widget.set_bool(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } @@ -356,7 +377,7 @@ where .map(|item| widget.set_value(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } @@ -388,7 +409,7 @@ where .map(|item| widget.set_value(item)) .unwrap_or(TkAction::EMPTY) } - fn handle_message(&self, mgr: &mut EventMgr, data: &Data, key: &Data::Key) { + fn on_message(&self, mgr: &mut EventMgr, _: &mut Self::Widget, data: &Data, key: &Data::Key) { if let Some(state) = mgr.try_pop_msg() { data.update(mgr, key, state); } diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs index e85f812d7..80a4afe08 100644 --- a/crates/kas-widgets/src/view/driver/config.rs +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -115,7 +115,13 @@ impl driver::Driver> for driver::DefaultView { | widget.touch_nav_focus.set_bool(data.touch_nav_focus) } - fn handle_message(&self, mgr: &mut EventMgr, data: &SharedRc, _: &()) { + fn on_message( + &self, + mgr: &mut EventMgr, + _: &mut Self::Widget, + data: &SharedRc, + _: &(), + ) { if let Some(msg) = mgr.try_pop_msg() { let mut data = data.update_mut(mgr); match msg { diff --git a/crates/kas-widgets/src/view/list_view.rs b/crates/kas-widgets/src/view/list_view.rs index 3629a09d0..2543f4776 100644 --- a/crates/kas-widgets/src/view/list_view.rs +++ b/crates/kas-widgets/src/view/list_view.rs @@ -44,8 +44,8 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`Driver::handle_message`] method is - /// called. After calling [`Driver::handle_message`], this widget attempts to + /// When a child pushes a message, the [`Driver::on_message`] method is + /// called. After calling [`Driver::on_message`], this widget attempts to /// read and handle [`SelectMsg`]. /// /// When selection is enabled and an item is selected or deselected, this @@ -747,12 +747,13 @@ impl_scope! { } fn handle_message(&mut self, mgr: &mut EventMgr, index: usize) { - let key = match self.widgets[index].key.clone() { + let w = &mut self.widgets[index]; + let key = match w.key.clone() { Some(k) => k, None => return, }; - self.view.handle_message(mgr, &self.data, &key); + self.view.on_message(mgr, &mut w.widget, &self.data, &key); if let Some(SelectMsg) = mgr.try_pop_msg() { match self.sel_mode { diff --git a/crates/kas-widgets/src/view/matrix_view.rs b/crates/kas-widgets/src/view/matrix_view.rs index e5423d082..c693fdc7b 100644 --- a/crates/kas-widgets/src/view/matrix_view.rs +++ b/crates/kas-widgets/src/view/matrix_view.rs @@ -50,8 +50,8 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`Driver::handle_message`] method is - /// called. After calling [`Driver::handle_message`], this widget attempts + /// When a child pushes a message, the [`Driver::on_message`] method is + /// called. After calling [`Driver::on_message`], this widget attempts /// to read and handle [`SelectMsg`]. /// /// When selection is enabled and an item is selected or deselected, this @@ -761,12 +761,13 @@ impl_scope! { } fn handle_message(&mut self, mgr: &mut EventMgr, index: usize) { - let key = match self.widgets[index].key.clone() { + let w = &mut self.widgets[index]; + let key = match w.key.clone() { Some(k) => k, None => return, }; - self.view.handle_message(mgr, &self.data, &key); + self.view.on_message(mgr, &mut w.widget, &self.data, &key); if let Some(SelectMsg) = mgr.try_pop_msg() { match self.sel_mode { diff --git a/crates/kas-widgets/src/view/single_view.rs b/crates/kas-widgets/src/view/single_view.rs index a503dc313..35bb8a74e 100644 --- a/crates/kas-widgets/src/view/single_view.rs +++ b/crates/kas-widgets/src/view/single_view.rs @@ -26,7 +26,7 @@ impl_scope! { /// /// # Messages /// - /// When a child pushes a message, the [`Driver::handle_message`] method is + /// When a child pushes a message, the [`Driver::on_message`] method is /// called. #[autoimpl(Debug ignore self.view)] #[derive(Clone)] @@ -128,7 +128,7 @@ impl_scope! { } fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { - self.view.handle_message(mgr, &self.data, &()); + self.view.on_message(mgr, &mut self.child, &self.data, &()); } } } diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index 4ef7cd00d..02dfbc425 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -191,7 +191,13 @@ impl Driver<(bool, String), MySharedData> for MyDriver { } } - fn handle_message(&self, mgr: &mut EventMgr, data: &MySharedData, key: &usize) { + fn on_message( + &self, + mgr: &mut EventMgr, + _: &mut Self::Widget, + data: &MySharedData, + key: &usize, + ) { if let Some(msg) = mgr.try_pop_msg() { let mut borrow = data.data.borrow_mut(); borrow.ver += 1; From 4f71a776b4c3836bae21d203eef60655714b8313 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 13:03:31 +0100 Subject: [PATCH 22/27] Gallery: add config tab Also: add Toolkit::event_config --- crates/kas-wgpu/src/lib.rs | 6 ++++++ examples/gallery.rs | 43 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/kas-wgpu/src/lib.rs b/crates/kas-wgpu/src/lib.rs index 9df24bd86..627e31d07 100644 --- a/crates/kas-wgpu/src/lib.rs +++ b/crates/kas-wgpu/src/lib.rs @@ -189,6 +189,12 @@ where &mut self.shared.draw } + /// Access event configuration + #[inline] + pub fn event_config(&self) -> &SharedRc { + &self.shared.config + } + /// Access the theme by ref #[inline] pub fn theme(&self) -> &T { diff --git a/examples/gallery.rs b/examples/gallery.rs index 1fbb3b503..8285b71bb 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -9,7 +9,7 @@ //! (excepting custom graphics). use kas::dir::Right; -use kas::event::VirtualKeyCode as VK; +use kas::event::{Config, VirtualKeyCode as VK}; use kas::model::SharedRc; use kas::prelude::*; use kas::resvg::Svg; @@ -428,6 +428,44 @@ Embedded GPU-rendered content is also possible (see separate Mandlebrot example) }) } +fn config(config: SharedRc) -> Box { + use kas::text::format::Markdown; + + const DESC: &str = "\ +Event configuration editor +================ + +Updated items should have immediate effect. + +To persist, set the following environment variables: +``` +KAS_CONFIG=config.yaml +KAS_CONFIG_MODE=readwrite +``` +"; + + Box::new(impl_singleton! { + #[widget{ + layout = column: [ + ScrollLabel::new(Markdown::new(DESC).unwrap()), + Separator::new(), + self.view, + ]; + }] + #[derive(Debug)] + struct { + core: widget_core!(), + #[widget] view: SingleView> = SingleView::new(config), + } + + impl SetDisabled for Self { + fn set_disabled(&mut self, mgr: &mut EventMgr, state: bool) { + mgr.set_disabled(self.view.id(), state); + } + } + }) +} + fn main() -> Result<(), Box> { env_logger::init(); @@ -508,7 +546,8 @@ fn main() -> Result<(), Box> { .with_title("Widgets", widgets()) //TODO: use img_gallery as logo .with_title("Text editor", editor()) .with_title("List", filter_list()) - .with_title("Canvas", canvas()), + .with_title("Canvas", canvas()) + .with_title("Config", config(toolkit.event_config().clone())), } impl Widget for Self { fn handle_message(&mut self, mgr: &mut EventMgr, _: usize) { From b3e61a6bb24c277a3ac4019e807fa1f5a4b50819 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 17:01:09 +0100 Subject: [PATCH 23/27] SizeMgr: add dpem, min_scroll_size; remove pixels_from_*, line_height --- crates/kas-core/src/theme/size.rs | 60 +++++++++++++------------------ crates/kas-theme/src/dim.rs | 22 +++++------- crates/kas-widgets/src/scroll.rs | 4 +-- 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/crates/kas-core/src/theme/size.rs b/crates/kas-core/src/theme/size.rs index 514ac1d87..67cbe9bb8 100644 --- a/crates/kas-core/src/theme/size.rs +++ b/crates/kas-core/src/theme/size.rs @@ -53,11 +53,17 @@ impl<'a> SizeMgr<'a> { SizeMgr(self.0) } - /// Get the scale (DPI) factor + /// Get the scale factor /// /// "Traditional" PC screens have a scale factor of 1; high-DPI screens - /// may have a factor of 2 or higher; this may be fractional. It is - /// recommended to calculate sizes as follows: + /// may have a factor of 2 or higher. This may be fractional and may be + /// adjusted to the user's taste. + /// + /// DPI is usually `96.0 * scale_factor` but this may be inaccurate (due to + /// user's preference or inaccurate screen size measurements or some form + /// of scaling used on mobile devices). + /// + /// It is recommended to calculate integer pixel sizes as follows: /// ``` /// use kas_core::cast::*; /// # let scale_factor = 1.5f32; @@ -71,21 +77,19 @@ impl<'a> SizeMgr<'a> { self.0.scale_factor() } - /// Convert a size in virtual pixels to physical pixels - pub fn pixels_from_virtual(&self, px: f32) -> f32 { - px * self.scale_factor() - } - - /// Convert a size in font Points to physical pixels - pub fn pixels_from_points(&self, pt: f32) -> f32 { - self.0.pixels_from_points(pt) + /// Get the Em size of the standard font in pixels + /// + /// The Em is a unit of typography, corresponding to the distance between + /// ascent and descent bounding lines (thus, the line height). + /// + /// This method returns the size of 1 Em in physical pixels. + pub fn dpem(&self) -> f32 { + self.0.dpem() } - /// Convert a size in font Em to physical pixels - /// - /// (This depends on the font size.) - pub fn pixels_from_em(&self, em: f32) -> f32 { - self.0.pixels_from_em(em) + /// The minimum size of a scrollable area + pub fn min_scroll_size(&self, axis: impl Directional) -> i32 { + self.0.min_scroll_size(axis.is_vertical()) } /// The margin around content within a widget @@ -121,15 +125,6 @@ impl<'a> SizeMgr<'a> { self.0.frame(style, axis.is_vertical()) } - /// The height of a line of text using the standard font - /// - /// Note: `self.pixels_from_em(1.0)` returns approximately the same value - /// and is faster since it only converts units. - /// This method reads font metrics. - pub fn line_height(&self, class: TextClass) -> i32 { - self.0.line_height(class) - } - /// Update a text object, setting font properties and getting a size bound /// /// This method updates the text's [`Environment`] and uses the result to @@ -154,16 +149,14 @@ impl<'a> SizeMgr<'a> { #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] #[autoimpl(for> R)] pub trait ThemeSize { - /// Get the scale (DPI) factor + /// Get the scale factor fn scale_factor(&self) -> f32; - /// Convert a size in font Points to physical pixels - fn pixels_from_points(&self, pt: f32) -> f32; + /// Get the Em size of the standard font in pixels + fn dpem(&self) -> f32; - /// Convert a size in font Em to physical pixels - /// - /// (This depends on the font size.) - fn pixels_from_em(&self, em: f32) -> f32; + /// The minimum size of a scrollable area + fn min_scroll_size(&self, axis_is_vertical: bool) -> i32; /// The margin around content within a widget /// @@ -194,9 +187,6 @@ pub trait ThemeSize { /// Size of a frame around another element fn frame(&self, style: FrameStyle, axis_is_vertical: bool) -> FrameRules; - /// The height of a line of text using the standard font - fn line_height(&self, class: TextClass) -> i32; - /// Update a text object, setting font properties and getting a size bound /// /// This method updates the text's [`Environment`] and uses the result to diff --git a/crates/kas-theme/src/dim.rs b/crates/kas-theme/src/dim.rs index e49994d46..a29ab40cf 100644 --- a/crates/kas-theme/src/dim.rs +++ b/crates/kas-theme/src/dim.rs @@ -160,12 +160,16 @@ impl ThemeSize for Window { self.dims.scale_factor } - fn pixels_from_points(&self, pt: f32) -> f32 { - self.dims.dpp * pt + fn dpem(&self) -> f32 { + self.dims.dpem } - fn pixels_from_em(&self, em: f32) -> f32 { - self.dims.dpem * em + fn min_scroll_size(&self, axis_is_vertical: bool) -> i32 { + if axis_is_vertical { + (self.dims.dpem * 3.0).cast_ceil() + } else { + self.dims.min_line_length + } } fn inner_margin(&self) -> Size { @@ -263,14 +267,6 @@ impl ThemeSize for Window { } } - fn line_height(&self, class: TextClass) -> i32 { - let font_id = self.fonts.get(&class).cloned().unwrap_or_default(); - kas::text::fonts::fonts() - .get_first_face(font_id) - .height(self.dims.dpem) - .cast_ceil() - } - fn text_bound(&self, text: &mut dyn TextApi, class: TextClass, axis: AxisInfo) -> SizeRules { let margin = match axis.is_horizontal() { true => self.dims.text_margin.0, @@ -324,7 +320,7 @@ impl ThemeSize for Window { let min = if matches!(class, TextClass::Label(true) | TextClass::AccelLabel(true)) { bound } else { - let line_height = self.line_height(class); + let line_height = self.dims.dpem.cast_ceil(); match class { _ if class.single_line() => line_height, TextClass::LabelScroll => bound.min(line_height * 3), diff --git a/crates/kas-widgets/src/scroll.rs b/crates/kas-widgets/src/scroll.rs index 9bda05f20..86318357f 100644 --- a/crates/kas-widgets/src/scroll.rs +++ b/crates/kas-widgets/src/scroll.rs @@ -7,7 +7,6 @@ use kas::event::{components::ScrollComponent, Scroll}; use kas::prelude::*; -use kas::theme::TextClass; use std::fmt::Debug; impl_scope! { @@ -94,8 +93,7 @@ impl_scope! { fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules { let mut rules = self.inner.size_rules(size_mgr.re(), axis); self.min_child_size.set_component(axis, rules.min_size()); - let line_height = size_mgr.line_height(TextClass::Label(false)); - rules.reduce_min_to(line_height); + rules.reduce_min_to(size_mgr.min_scroll_size(axis)); // We use a frame to contain the content margin within the scrollable area. let frame = kas::layout::FrameRules::new(0, 0, 0, (0, 0)); From 8932eb8b71f62d5f654eb7941d445d7660824710 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 17:13:49 +0100 Subject: [PATCH 24/27] Revise handling of scroll distance --- crates/kas-core/src/event/components.rs | 4 +- crates/kas-core/src/event/config.rs | 53 ++++++++----------- crates/kas-core/src/event/manager.rs | 1 - .../kas-core/src/event/manager/mgr_shell.rs | 13 +++-- crates/kas-wgpu/src/window.rs | 9 ++-- crates/kas-widgets/src/edit.rs | 6 +-- crates/kas-widgets/src/scroll_label.rs | 6 +-- 7 files changed, 38 insertions(+), 54 deletions(-) diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index 050023ea7..d75d85991 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -251,7 +251,7 @@ impl ScrollComponent { _ => return (false, Response::Unused), }; let delta = match delta { - LineDelta(x, y) => mgr.config().scroll_distance((-x, y), None), + LineDelta(x, y) => mgr.config().scroll_distance((x, y)), PixelDelta(d) => d, }; self.offset - delta @@ -266,7 +266,7 @@ impl ScrollComponent { } Event::Scroll(delta) => { let delta = match delta { - LineDelta(x, y) => mgr.config().scroll_distance((-x, y), None), + LineDelta(x, y) => mgr.config().scroll_distance((x, y)), PixelDelta(d) => d, }; moved = self.scroll_by_delta(mgr, delta); diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/event/config.rs index 93a05b5d0..4804bd2d7 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/event/config.rs @@ -42,9 +42,6 @@ pub struct Config { #[cfg_attr(feature = "config", serde(default = "defaults::touch_select_delay_ms"))] pub touch_select_delay_ms: u32, - #[cfg_attr(feature = "config", serde(default = "defaults::scroll_lines"))] - pub scroll_lines: f32, - #[cfg_attr( feature = "config", serde(default = "defaults::scroll_flick_timeout_ms") @@ -57,6 +54,9 @@ pub struct Config { #[cfg_attr(feature = "config", serde(default = "defaults::scroll_flick_sub"))] pub scroll_flick_sub: f32, + #[cfg_attr(feature = "config", serde(default = "defaults::scroll_dist_em"))] + pub scroll_dist_em: f32, + #[cfg_attr(feature = "config", serde(default = "defaults::pan_dist_thresh"))] pub pan_dist_thresh: f32, @@ -79,10 +79,10 @@ impl Default for Config { Config { menu_delay_ms: defaults::menu_delay_ms(), touch_select_delay_ms: defaults::touch_select_delay_ms(), - scroll_lines: defaults::scroll_lines(), scroll_flick_timeout_ms: defaults::scroll_flick_timeout_ms(), scroll_flick_mul: defaults::scroll_flick_mul(), scroll_flick_sub: defaults::scroll_flick_sub(), + scroll_dist_em: defaults::scroll_dist_em(), pan_dist_thresh: defaults::pan_dist_thresh(), mouse_pan: defaults::mouse_pan(), mouse_text_pan: defaults::mouse_text_pan(), @@ -105,8 +105,8 @@ impl Config { #[derive(Clone, Debug)] pub struct WindowConfig { pub(crate) config: SharedRc, - scroll_dist: f32, scroll_flick_sub: f32, + scroll_dist: f32, pan_dist_thresh: f32, } @@ -114,25 +114,24 @@ impl WindowConfig { /// Construct #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn new(config: SharedRc, scale_factor: f32) -> Self { + pub fn new(config: SharedRc, scale_factor: f32, dpem: f32) -> Self { let mut w = WindowConfig { config, - scroll_dist: f32::NAN, scroll_flick_sub: f32::NAN, + scroll_dist: f32::NAN, pan_dist_thresh: f32::NAN, }; - w.update(scale_factor); + w.update(scale_factor, dpem); w } /// Update #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))] - pub fn update(&mut self, scale_factor: f32) { + pub fn update(&mut self, scale_factor: f32, dpem: f32) { let base = self.config.borrow(); - const LINE_HEIGHT: f32 = 19.0; // TODO: maybe we shouldn't assume this? - self.scroll_dist = base.scroll_lines * LINE_HEIGHT; self.scroll_flick_sub = base.scroll_flick_sub * scale_factor; + self.scroll_dist = base.scroll_dist_em * dpem; self.pan_dist_thresh = base.pan_dist_thresh * scale_factor; } } @@ -150,23 +149,6 @@ impl WindowConfig { Duration::from_millis(self.config.borrow().touch_select_delay_ms.cast()) } - /// Get distance in pixels to scroll due to mouse wheel - /// - /// Calculates scroll distance from `(horiz, vert)` lines. - /// - /// If `line_height` is provided, scroll distance is based on this value, - /// otherwise it is based on an arbitrary line height. - pub fn scroll_distance(&self, lines: (f32, f32), line_height: Option) -> Offset { - let dist = match line_height { - Some(height) => height * self.config.borrow().scroll_lines, - None => self.scroll_dist, - }; - Offset( - (dist * lines.0).cast_nearest(), - (dist * lines.1).cast_nearest(), - ) - } - /// Controls activation of glide/momentum scrolling /// /// This is the maximum time between the last press-movement and final @@ -192,6 +174,15 @@ impl WindowConfig { (self.config.borrow().scroll_flick_mul, self.scroll_flick_sub) } + /// Get distance in pixels to scroll due to mouse wheel + /// + /// Calculates scroll distance from `(horiz, vert)` lines. + pub fn scroll_distance(&self, lines: (f32, f32)) -> Offset { + let x = (self.scroll_dist * -lines.0).cast_nearest(); + let y = (self.scroll_dist * lines.1).cast_nearest(); + Offset(x, y) + } + /// Drag distance threshold before panning (scrolling) starts /// /// When the distance moved is greater than this threshold, panning should @@ -279,9 +270,6 @@ mod defaults { pub fn touch_select_delay_ms() -> u32 { 1000 } - pub fn scroll_lines() -> f32 { - 3.0 - } pub fn scroll_flick_timeout_ms() -> u32 { 25 } @@ -291,6 +279,9 @@ mod defaults { pub fn scroll_flick_sub() -> f32 { 100.0 } + pub fn scroll_dist_em() -> f32 { + 3.0 + } pub fn pan_dist_thresh() -> f32 { 2.1 } diff --git a/crates/kas-core/src/event/manager.rs b/crates/kas-core/src/event/manager.rs index d2f339b13..0b67895dc 100644 --- a/crates/kas-core/src/event/manager.rs +++ b/crates/kas-core/src/event/manager.rs @@ -148,7 +148,6 @@ type AccelLayer = (bool, HashMap); #[derive(Debug)] pub struct EventState { config: WindowConfig, - scale_factor: f32, disabled: Vec, window_has_focus: bool, modifiers: ModifiersState, diff --git a/crates/kas-core/src/event/manager/mgr_shell.rs b/crates/kas-core/src/event/manager/mgr_shell.rs index 3d907aad5..f26451ac6 100644 --- a/crates/kas-core/src/event/manager/mgr_shell.rs +++ b/crates/kas-core/src/event/manager/mgr_shell.rs @@ -26,10 +26,9 @@ const FAKE_MOUSE_BUTTON: MouseButton = MouseButton::Other(0); impl EventState { /// Construct an event manager per-window data struct #[inline] - pub fn new(config: SharedRc, scale_factor: f32) -> Self { + pub fn new(config: SharedRc, scale_factor: f32, dpem: f32) -> Self { EventState { - config: WindowConfig::new(config, scale_factor), - scale_factor, + config: WindowConfig::new(config, scale_factor, dpem), disabled: vec![], window_has_focus: false, modifiers: ModifiersState::empty(), @@ -57,9 +56,8 @@ impl EventState { } /// Update scale factor - pub fn set_scale_factor(&mut self, scale_factor: f32) { - self.config.update(scale_factor); - self.scale_factor = scale_factor; + pub fn set_scale_factor(&mut self, scale_factor: f32, dpem: f32) { + self.config.update(scale_factor, dpem); } /// Configure event manager for a widget tree. @@ -255,7 +253,8 @@ impl<'a> EventMgr<'a> { /// Update widgets with an [`UpdateId`] pub fn update_widgets(&mut self, widget: &mut dyn Widget, id: UpdateId, payload: u64) { if id == self.state.config.config.id() { - self.state.config.update(self.scale_factor); + let (sf, dpem) = self.size_mgr(|size| (size.scale_factor(), size.dpem())); + self.state.config.update(sf, dpem); } let start = Instant::now(); diff --git a/crates/kas-wgpu/src/window.rs b/crates/kas-wgpu/src/window.rs index e22a126ec..a3a4b4990 100644 --- a/crates/kas-wgpu/src/window.rs +++ b/crates/kas-wgpu/src/window.rs @@ -77,8 +77,9 @@ impl>> Window { }; let mut theme_window = shared.theme.new_window(scale_factor); + let dpem = theme_window.size().dpem(); - let mut ev_state = EventState::new(shared.config.clone(), scale_factor); + let mut ev_state = EventState::new(shared.config.clone(), scale_factor, dpem); let mut tkw = TkWindow::new(shared, None, &mut theme_window); ev_state.full_configure(&mut tkw, widget.as_widget_mut()); @@ -123,8 +124,9 @@ impl>> Window { // Now that we have a scale factor, we may need to resize: if use_logical_size && scale_factor != 1.0 { let scale_factor = scale_factor as f32; - ev_state.set_scale_factor(scale_factor); shared.theme.update_window(&mut theme_window, scale_factor); + let dpem = theme_window.size().dpem(); + ev_state.set_scale_factor(scale_factor, dpem); solve_cache.invalidate_rule_cache(); } @@ -173,10 +175,11 @@ impl>> Window { // Note: API allows us to set new window size here. shared.scale_factor = scale_factor; let scale_factor = scale_factor as f32; - self.ev_state.set_scale_factor(scale_factor); shared .theme .update_window(&mut self.theme_window, scale_factor); + let dpem = self.theme_window.size().dpem(); + self.ev_state.set_scale_factor(scale_factor, dpem); self.solve_cache.invalidate_rule_cache(); self.do_resize(shared, *new_inner_size); } diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index 7e50dbe8b..7fb0aeb5c 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -483,11 +483,7 @@ impl_scope! { }, Event::Scroll(delta) => { let delta2 = match delta { - ScrollDelta::LineDelta(x, y) => { - // We arbitrarily scroll 3 Em: - let dist = 3.0 * self.text.env().dpem; - Offset((x * dist).cast_nearest(), (y * dist).cast_nearest()) - } + ScrollDelta::LineDelta(x, y) => mgr.config().scroll_distance((x, y)), ScrollDelta::PixelDelta(coord) => coord, }; self.pan_delta(mgr, delta2) diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index a3631e755..5d96f76d5 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -189,11 +189,7 @@ impl_scope! { } Event::Scroll(delta) => { let delta2 = match delta { - ScrollDelta::LineDelta(x, y) => { - // We arbitrarily scroll 3 Em: - let dist = 3.0 * self.text.env().dpem; - Offset((x * dist).cast_nearest(), (y * dist).cast_nearest()) - } + ScrollDelta::LineDelta(x, y) => mgr.config().scroll_distance((x, y)), ScrollDelta::PixelDelta(coord) => coord, }; self.pan_delta(mgr, delta2) From b577da523a337fb6526166f73ba57d240b205454 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 12 Jul 2022 12:08:18 +0100 Subject: [PATCH 25/27] Add scroll_dist_em to driver/config; adjust default config values --- crates/kas-core/src/event/config.rs | 10 +++---- crates/kas-widgets/src/view/driver/config.rs | 29 ++++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/event/config.rs index 4804bd2d7..8ed733f83 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/event/config.rs @@ -271,19 +271,19 @@ mod defaults { 1000 } pub fn scroll_flick_timeout_ms() -> u32 { - 25 + 50 } pub fn scroll_flick_mul() -> f32 { - 0.5 + 0.625 } pub fn scroll_flick_sub() -> f32 { - 100.0 + 200.0 } pub fn scroll_dist_em() -> f32 { - 3.0 + 4.5 } pub fn pan_dist_thresh() -> f32 { - 2.1 + 5.0 } pub fn mouse_pan() -> MousePan { MousePan::Always diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs index 80a4afe08..7e2bedb7b 100644 --- a/crates/kas-widgets/src/view/driver/config.rs +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -18,6 +18,7 @@ enum Msg { ScrollFlickTimeout(u32), ScrollFlickMul(f32), ScrollFlickSub(f32), + ScrollDistEm(f32), PanDistThresh(f32), MousePan(MousePan), MouseTextPan(MousePan), @@ -35,12 +36,13 @@ impl_scope! { 0, 2: "Scroll flick timeout:"; 1, 2: self.scroll_flick_timeout; 2, 2: "ms"; 0, 3: "Scroll flick multiply:"; 1, 3: self.scroll_flick_mul; 0, 4: "Scroll flick subtract:"; 1, 4: self.scroll_flick_sub; - 0, 5: "Pan distance threshold:"; 1, 5: self.pan_dist_thresh; - 0, 6: "Mouse pan:"; 1..3, 6: self.mouse_pan; - 0, 7: "Mouse text pan:"; 1..3, 7: self.mouse_text_pan; - 1..3, 8: self.mouse_nav_focus; - 1..3, 9: self.touch_nav_focus; - 0, 10: "Restore default values:"; 1..3, 10: TextButton::new_msg("Reset", Msg::Reset); + 0, 5: "Scroll wheel distance:"; 1, 5: self.scroll_dist_em; 2, 5: "em"; + 0, 6: "Pan distance threshold:"; 1, 6: self.pan_dist_thresh; + 0, 7: "Mouse pan:"; 1..3, 7: self.mouse_pan; + 0, 8: "Mouse text pan:"; 1..3, 8: self.mouse_text_pan; + 1..3, 9: self.mouse_nav_focus; + 1..3, 10: self.touch_nav_focus; + 0, 11: "Restore default values:"; 1..3, 11: TextButton::new_msg("Reset", Msg::Reset); }; }] #[derive(Debug)] @@ -51,6 +53,7 @@ impl_scope! { #[widget] scroll_flick_timeout: Spinner, #[widget] scroll_flick_mul: Spinner, #[widget] scroll_flick_sub: Spinner, + #[widget] scroll_dist_em: Spinner, #[widget] pan_dist_thresh: Spinner, #[widget] mouse_pan: ComboBox, #[widget] mouse_text_pan: ComboBox, @@ -72,17 +75,19 @@ impl driver::Driver> for driver::DefaultView { EventConfig { core: Default::default(), - menu_delay: Spinner::new(0..=10_000, 50) + menu_delay: Spinner::new(0..=5_000, 50) .on_change(|mgr, v| mgr.push_msg(Msg::MenuDelay(v))), - touch_select_delay: Spinner::new(0..=10_000, 50) + touch_select_delay: Spinner::new(0..=5_000, 50) .on_change(|mgr, v| mgr.push_msg(Msg::TouchSelectDelay(v))), - scroll_flick_timeout: Spinner::new(0..=1_000, 5) + scroll_flick_timeout: Spinner::new(0..=500, 5) .on_change(|mgr, v| mgr.push_msg(Msg::ScrollFlickTimeout(v))), - scroll_flick_mul: Spinner::new(0.0..=1.0, 0.0625) + scroll_flick_mul: Spinner::new(0.0..=1.0, 0.03125) .on_change(|mgr, v| mgr.push_msg(Msg::ScrollFlickMul(v))), scroll_flick_sub: Spinner::new(0.0..=1.0e4, 10.0) .on_change(|mgr, v| mgr.push_msg(Msg::ScrollFlickSub(v))), - pan_dist_thresh: Spinner::new(0.125..=10.0, 0.125) + scroll_dist_em: Spinner::new(0.125..=125.0, 0.125) + .on_change(|mgr, v| mgr.push_msg(Msg::ScrollDistEm(v))), + pan_dist_thresh: Spinner::new(0.25..=25.0, 0.25) .on_change(|mgr, v| mgr.push_msg(Msg::PanDistThresh(v))), mouse_pan: mouse_pan .clone() @@ -106,6 +111,7 @@ impl driver::Driver> for driver::DefaultView { .set_value(data.scroll_flick_timeout_ms) | widget.scroll_flick_mul.set_value(data.scroll_flick_mul) | widget.scroll_flick_sub.set_value(data.scroll_flick_sub) + | widget.scroll_dist_em.set_value(data.scroll_dist_em) | widget.pan_dist_thresh.set_value(data.pan_dist_thresh) | widget.mouse_pan.set_active(data.mouse_pan as usize) | widget @@ -130,6 +136,7 @@ impl driver::Driver> for driver::DefaultView { Msg::ScrollFlickTimeout(v) => data.scroll_flick_timeout_ms = v, Msg::ScrollFlickMul(v) => data.scroll_flick_mul = v, Msg::ScrollFlickSub(v) => data.scroll_flick_sub = v, + Msg::ScrollDistEm(v) => data.scroll_dist_em = v, Msg::PanDistThresh(v) => data.pan_dist_thresh = v, Msg::MousePan(v) => data.mouse_pan = v, Msg::MouseTextPan(v) => data.mouse_text_pan = v, From f598ca6744162eda9c5748ee7855855de6b80268 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 13 Jul 2022 18:11:25 +0100 Subject: [PATCH 26/27] Add event::Config::is_dirty field --- crates/kas-core/src/event/config.rs | 6 +++++- crates/kas-widgets/src/view/driver/config.rs | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/kas-core/src/event/config.rs b/crates/kas-core/src/event/config.rs index 8ed733f83..3ee6a2a00 100644 --- a/crates/kas-core/src/event/config.rs +++ b/crates/kas-core/src/event/config.rs @@ -72,6 +72,9 @@ pub struct Config { #[cfg_attr(feature = "config", serde(default = "Shortcuts::platform_defaults"))] pub shortcuts: Shortcuts, + + #[cfg_attr(feature = "config", serde(skip))] + pub is_dirty: bool, } impl Default for Config { @@ -89,6 +92,7 @@ impl Default for Config { mouse_nav_focus: defaults::mouse_nav_focus(), touch_nav_focus: defaults::touch_nav_focus(), shortcuts: Shortcuts::platform_defaults(), + is_dirty: false, } } } @@ -97,7 +101,7 @@ impl Config { /// Has the config ever been updated? #[inline] pub fn is_dirty(&self) -> bool { - false // current code never updates config + self.is_dirty } } diff --git a/crates/kas-widgets/src/view/driver/config.rs b/crates/kas-widgets/src/view/driver/config.rs index 7e2bedb7b..3a59036b3 100644 --- a/crates/kas-widgets/src/view/driver/config.rs +++ b/crates/kas-widgets/src/view/driver/config.rs @@ -144,6 +144,7 @@ impl driver::Driver> for driver::DefaultView { Msg::TouchNavFocus(v) => data.touch_nav_focus = v, Msg::Reset => *data = Config::default(), } + data.is_dirty = true; } } } From 7fb94fb3d185473aab44cc6458fae0d022148d76 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 14 Jul 2022 07:48:31 +0100 Subject: [PATCH 27/27] Clippy fix --- crates/kas-widgets/src/spinner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index ff317384f..279b7f6eb 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -186,7 +186,7 @@ impl_scope! { edit: EditField::new("").with_guard(SpinnerGuard::new(range)), b_up: MarkButton::new(MarkStyle::Point(Direction::Up), SpinBtn::Up), b_down: MarkButton::new(MarkStyle::Point(Direction::Down), SpinBtn::Down), - step: step, + step, on_change: None, } }