diff --git a/crates/kas-core/src/core/data.rs b/crates/kas-core/src/core/data.rs index 4f6333bd9..8def7e2c1 100644 --- a/crates/kas-core/src/core/data.rs +++ b/crates/kas-core/src/core/data.rs @@ -117,5 +117,7 @@ pub trait Window: Widget { /// This allows for actions on destruction. /// /// Default: do nothing. - fn handle_closure(&mut self, _mgr: &mut EventMgr) {} + fn handle_closure(&mut self, mgr: &mut EventMgr) { + let _ = mgr; + } } diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs index 325445cd4..d8518e800 100644 --- a/crates/kas-core/src/core/widget.rs +++ b/crates/kas-core/src/core/widget.rs @@ -19,16 +19,18 @@ use kas_macros::autoimpl; use crate::event::EventState; #[allow(unused)] use crate::layout::{self, AutoLayout}; +#[allow(unused)] +use crate::TkAction; +#[allow(unused)] +use kas_macros as macros; -/// Base widget functionality +/// Base functionality for [`Widget`]s /// -/// See the [`Widget`] trait for documentation of the widget family. +/// # Implementing WidgetCore /// -/// This trait **must** be implement by the [`derive(Widget)`] macro. -/// Users **must not** implement this `WidgetCore` trait manually or may face -/// unexpected breaking changes. -/// -/// [`derive(Widget)`]: https://docs.rs/kas/latest/kas/macros/index.html#the-derivewidget-macro +/// Implementations of this trait are generated via macro. +/// **Directly implementing this trait is not supported**. +/// See [`Widget`] trait documentation. #[autoimpl(for<'a, T: trait + ?Sized> &'a mut T, Box)] pub trait WidgetCore: fmt::Debug { /// Get the widget's identifier @@ -50,17 +52,25 @@ pub trait WidgetCore: fmt::Debug { fn as_widget_mut(&mut self) -> &mut dyn Widget; } -/// Listing of a widget's children +/// Listing of a [`Widget`]'s children /// -/// This trait is part of the [`Widget`] family and is derived by -/// [`derive(Widget)`] unless `#[widget(children = noauto)]` is used. +/// This trait enumerates child widgets (that is, components of the widget which +/// are themselves widgets). /// -/// Dynamic widgets must implement this trait manually, since [`derive(Widget)`] -/// cannot currently handle fields like `Vec`. Additionally, any -/// parent adding child widgets must ensure they get configured by calling -/// [`SetRectMgr::configure`]. +/// Enumerated widgets are automatically configured, via recursion, when their +/// parent is. See [`Widget::configure`]. /// -/// [`derive(Widget)`]: https://docs.rs/kas/latest/kas/macros/index.html#the-derivewidget-macro +/// # Implementing WidgetChildren +/// +/// Implementations of this trait are usually generated via macro. +/// See [`Widget`] trait documentation. +/// +/// In a few cases, namely widgets which may add/remove children dynamically, +/// this trait should be implemented directly. +/// +/// Note that parents are responsible for ensuring that newly added children +/// get configured, either by sending [`TkAction::RECONFIGURE`] by calling +/// [`SetRectMgr::configure`]. #[autoimpl(for<'a, T: trait + ?Sized> &'a mut T, Box)] pub trait WidgetChildren: WidgetCore { /// Get the number of child widgets @@ -99,16 +109,24 @@ pub trait WidgetChildren: WidgetCore { } } -/// Positioning and drawing routines for widgets +/// Positioning and drawing routines for [`Widget`]s /// /// This trait is related to [`Widget`], but may be used independently. /// -/// There are two methods of implementing this trait: +/// # Implementing Layout /// -/// - Use the `#[widget{ layout = .. }]` property (see [`#[widget]`] documentation) -/// - Implement manually. When part of a widget, the [`Self::set_rect`] and -/// [`Self::find_id`] methods gain default implementations (generated by the -/// [`#[widget]`] macro). +/// There are three cases: +/// +/// - For a non-widget, all methods must be implemented directly. +/// - For a [`Widget`] without using the `layout` macro property, +/// the [`Self::set_rect`] and [`Self::find_id`] methods gain default +/// implementations (generated via macro). +/// - For a [`Widget`] where the `#[widget{ layout = .. }]` property +/// is set (see [`macros::widget`] documentation), all methods have a +/// default implementation. Custom implementations may use [`AutoLayout`] to +/// access these default implementations. +/// +/// # Solving layout /// /// Layout is resolved as follows: /// @@ -118,15 +136,9 @@ pub trait WidgetChildren: WidgetCore { /// 4. [`Self::find_id`] may be used to find the widget under the mouse and [`Self::draw`] to draw /// elements. /// -/// Two methods of setting layout are possible: -/// -/// 1. Use [`layout::solve_size_rules`] or [`layout::SolveCache`] to solve and -/// set layout. This functions by calling [`Self::size_rules`] for each -/// axis then calling [`Self::set_rect`]. -/// 2. Only call [`Self::set_rect`]. For some widgets this is fine but for -/// others the internal layout will be incorrect. -/// -/// [`#[widget]`]: kas_macros::widget +/// Usually, [`Layout::size_rules`] methods are called recursively. To instead +/// solve layout for a single widget/layout object, it may be useful to use +/// [`layout::solve_size_rules`] or [`layout::SolveCache`]. #[autoimpl(for<'a, T: trait + ?Sized> &'a mut T, Box)] pub trait Layout { /// Get size rules for the given axis @@ -240,28 +252,132 @@ pub trait Layout { fn draw(&mut self, draw: DrawMgr); } -/// Widget trait +/// The Widget trait /// -/// Widgets must implement a family of traits, of which this trait is the final +/// Widgets implement a family of traits, of which this trait is the final /// member: /// -/// - [`WidgetCore`] — base functionality (this trait is *always* derived) -/// - [`WidgetChildren`] — enumerates children and provides methods derived -/// from this -/// - [`Layout`] — handles sizing and positioning of self and children -/// - [`Widget`] — the final trait +/// - [`WidgetCore`] — base functionality +/// - [`WidgetChildren`] — enumerates children +/// - [`Layout`] — handles sizing and positioning for self and children +/// - [`Widget`] — configuration, some aspects of layout, event handling +/// +/// # Implementing Widget +/// +/// To implement a widget, use the [`macros::widget`] macro. **This is the +/// only supported method of implementing `Widget`.** +/// +/// The [`macros::widget`] macro only works within [`macros::impl_scope`]. +/// Other trait implementations can be detected within this scope: /// -/// Widgets **must** use the [`derive(Widget)`] macro to implement at least -/// [`WidgetCore`] and [`Widget`]; these two traits **must not** be implemented -/// manually or users may face unexpected breaking changes. -/// This macro can optionally implement *all* above traits, and by default will -/// implement *all except for `Layout`*. This opt-out derive behaviour means -/// that adding additional traits into the family is not a breaking change. +/// - [`WidgetCore`] is always generated +/// - [`WidgetChildren`] is generated if no direct implementation is present +/// - [`Layout`] is generated if the `layout` attribute property is set, and +/// no direct implementation is found. In other cases where a direct +/// implementation of the trait is found, (default) method implementations +/// may be injected where not already present. +/// - [`Widget`] is generated if no direct implementation is present, +/// otherwise some (default) method implementations are injected where +/// these methods are not directly implemented. /// -/// To refer to a widget via dyn trait, use `&dyn Widget`. -/// To refer to a widget in generic functions, use ``. +/// Some simple examples follow. See also +/// [examples apps](https://github.com/kas-gui/kas/tree/master/examples) +/// and [`kas_widgets`](https://docs.rs/kas-widgets/latest/kas_widgets/) code. +/// ``` +/// # extern crate kas_core as kas; +/// use kas::prelude::*; +/// use kas::event; +/// use kas::theme::TextClass; +/// use std::fmt::Debug; /// -/// [`derive(Widget)`]: https://docs.rs/kas/latest/kas/macros/index.html#the-derivewidget-macro +/// impl_scope! { +/// /// A text label +/// #[derive(Clone, Debug)] +/// #[widget] +/// pub struct AccelLabel { +/// core: widget_core!(), +/// class: TextClass, +/// label: Text, +/// } +/// +/// impl Self { +/// /// Construct from `label` +/// pub fn new(label: impl Into) -> Self { +/// AccelLabel { +/// core: Default::default(), +/// class: TextClass::AccelLabel(true), +/// label: Text::new_multi(label.into()), +/// } +/// } +/// +/// /// Set text class (inline) +/// pub fn with_class(mut self, class: TextClass) -> Self { +/// self.class = class; +/// self +/// } +/// +/// /// Get the accelerator keys +/// pub fn keys(&self) -> &[event::VirtualKeyCode] { +/// self.label.text().keys() +/// } +/// } +/// +/// impl Layout for Self { +/// 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 SetRectMgr, 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_effects(self.rect().pos, &self.label, self.class); +/// } +/// } +/// } +/// +/// impl_scope! { +/// /// A push-button with a text label +/// #[derive(Debug)] +/// #[widget { +/// layout = button: self.label; +/// key_nav = true; +/// hover_highlight = true; +/// }] +/// pub struct TextButton { +/// core: widget_core!(), +/// #[widget] +/// label: AccelLabel, +/// message: M, +/// } +/// +/// impl Self { +/// /// Construct a button with given `label` +/// pub fn new(label: impl Into, message: M) -> Self { +/// TextButton { +/// core: Default::default(), +/// label: AccelLabel::new(label).with_class(TextClass::Button), +/// message, +/// } +/// } +/// } +/// impl Widget for Self { +/// fn configure(&mut self, mgr: &mut SetRectMgr) { +/// mgr.add_accel_keys(self.id_ref(), self.label.keys()); +/// } +/// +/// fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { +/// event.on_activate(mgr, self.id(), |mgr| { +/// mgr.push_msg(self.message.clone()); +/// Response::Used +/// }) +/// } +/// } +/// } +/// ``` #[autoimpl(for<'a, T: trait + ?Sized> &'a mut T, Box)] pub trait Widget: WidgetChildren + Layout { /// Make an identifier for a child @@ -395,6 +511,19 @@ pub trait Widget: WidgetChildren + Layout { Response::Unused } + /// Potentially steal an event before it reaches a child + /// + /// This is called on each widget while sending an event, including when the + /// target is self. + /// If this returns [`Response::Used`], the event is not sent further. + /// + /// Default implementation: return [`Response::Unused`]. + #[inline] + fn steal_event(&mut self, mgr: &mut EventMgr, id: &WidgetId, event: &Event) -> Response { + let _ = (mgr, id, event); + Response::Unused + } + /// Handle an event sent to child `index` but left unhandled /// /// Default implementation: call [`Self::handle_event`] with `event`. diff --git a/crates/kas-core/src/event/manager.rs b/crates/kas-core/src/event/manager.rs index 4346af4b5..4d014ba5d 100644 --- a/crates/kas-core/src/event/manager.rs +++ b/crates/kas-core/src/event/manager.rs @@ -603,7 +603,9 @@ impl<'a> EventMgr<'a> { event: Event, ) -> Response { let mut response = Response::Unused; - if let Some(index) = widget.find_child_index(&id) { + if widget.steal_event(self, &id, &event) == Response::Used { + response = Response::Used; + } else if let Some(index) = widget.find_child_index(&id) { let translation = widget.translation(); if disabled { // event is unused @@ -617,7 +619,6 @@ impl<'a> EventMgr<'a> { "Widget {} found index {index} for {id}, but child not found", widget.identify() ); - return Response::Unused; } if matches!(response, Response::Unused) { @@ -634,7 +635,6 @@ impl<'a> EventMgr<'a> { response |= widget.handle_event(self, event) } else { warn!("Widget {} cannot find path to {id}", widget.identify()); - return Response::Unused; } response diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index c13929418..ffa541ef4 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -191,8 +191,8 @@ impl<'a> DrawMgr<'a> { /// /// [`SizeMgr::text_bound`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). - pub fn text(&mut self, pos: Coord, text: &TextDisplay, class: TextClass) { - self.h.text(&self.id, pos, text, class); + pub fn text(&mut self, pos: Coord, text: impl AsRef, class: TextClass) { + self.h.text(&self.id, pos, text.as_ref(), class); } /// Draw text with effects @@ -212,10 +212,10 @@ impl<'a> DrawMgr<'a> { /// Other than visually highlighting the selection, this method behaves /// identically to [`Self::text`]. It is likely to be replaced in the /// future by a higher-level API. - pub fn text_selected, R: RangeBounds>( + pub fn text_selected>( &mut self, pos: Coord, - text: T, + text: impl AsRef, range: R, class: TextClass, ) { @@ -238,8 +238,15 @@ impl<'a> DrawMgr<'a> { /// /// [`SizeMgr::text_bound`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). - pub fn text_cursor(&mut self, pos: Coord, text: &TextDisplay, class: TextClass, byte: usize) { - self.h.text_cursor(&self.id, pos, text, class, byte); + pub fn text_cursor( + &mut self, + pos: Coord, + text: impl AsRef, + class: TextClass, + byte: usize, + ) { + self.h + .text_cursor(&self.id, pos, text.as_ref(), class, byte); } /// Draw UI element: checkbox @@ -264,13 +271,13 @@ impl<'a> DrawMgr<'a> { } /// Draw UI element: scrollbar - pub fn scrollbar(&mut self, track_rect: Rect, handle: &dyn Widget, dir: Direction) { + pub fn scrollbar(&mut self, track_rect: Rect, handle: &W, dir: Direction) { self.h .scrollbar(&self.id, handle.id_ref(), track_rect, handle.rect(), dir); } /// Draw UI element: slider - pub fn slider(&mut self, track_rect: Rect, handle: &dyn Widget, dir: Direction) { + pub fn slider(&mut self, track_rect: Rect, handle: &W, dir: Direction) { self.h .slider(&self.id, handle.id_ref(), track_rect, handle.rect(), dir); } diff --git a/crates/kas-core/src/toolkit.rs b/crates/kas-core/src/toolkit.rs index addeb3bdd..f1ee863f8 100644 --- a/crates/kas-core/src/toolkit.rs +++ b/crates/kas-core/src/toolkit.rs @@ -75,7 +75,7 @@ bitflags! { const RESIZE = 1 << 9; /// Update theme memory const THEME_UPDATE = 1 << 10; - /// Window requires reconfiguring + /// Reconfigure all widgets of the window /// /// *Configuring* widgets assigns [`WidgetId`] identifiers and calls /// [`crate::Widget::configure`]. diff --git a/crates/kas-theme/src/colors.rs b/crates/kas-theme/src/colors.rs index 1db72e908..4749020f4 100644 --- a/crates/kas-theme/src/colors.rs +++ b/crates/kas-theme/src/colors.rs @@ -230,7 +230,7 @@ impl ColorsSrgb { is_dark: false, background: Rgba8Srgb::from_str("#FAFAFA").unwrap(), frame: Rgba8Srgb::from_str("#BCBCBC").unwrap(), - accent: Rgba8Srgb::from_str("#7E3FF2").unwrap(), + accent: Rgba8Srgb::from_str("#8347f2").unwrap(), accent_soft: Rgba8Srgb::from_str("#B38DF9").unwrap(), nav_focus: Rgba8Srgb::from_str("#7E3FF2").unwrap(), edit_bg: Rgba8Srgb::from_str("#FAFAFA").unwrap(), @@ -268,7 +268,7 @@ impl ColorsSrgb { is_dark: false, background: Rgba8Srgb::from_str("#FFFFFF").unwrap(), frame: Rgba8Srgb::from_str("#DADADA").unwrap(), - accent: Rgba8Srgb::from_str("#7CDAFF").unwrap(), + accent: Rgba8Srgb::from_str("#3fafd7").unwrap(), accent_soft: Rgba8Srgb::from_str("#7CDAFF").unwrap(), nav_focus: Rgba8Srgb::from_str("#3B697A").unwrap(), edit_bg: Rgba8Srgb::from_str("#FFFFFF").unwrap(), diff --git a/crates/kas-theme/src/flat_theme.rs b/crates/kas-theme/src/flat_theme.rs index 6b3132445..046dfc717 100644 --- a/crates/kas-theme/src/flat_theme.rs +++ b/crates/kas-theme/src/flat_theme.rs @@ -628,6 +628,8 @@ where fn mark(&mut self, id: &WidgetId, rect: Rect, style: MarkStyle) { let col = if self.ev.is_disabled(id) { self.cols.text_disabled + } else if self.ev.is_hovered(id) { + self.cols.accent } else { self.cols.text }; diff --git a/crates/kas-widgets/src/button.rs b/crates/kas-widgets/src/button.rs index 19169cfda..75ff517ea 100644 --- a/crates/kas-widgets/src/button.rs +++ b/crates/kas-widgets/src/button.rs @@ -22,6 +22,8 @@ impl_scope! { #[derive(Clone)] #[widget { layout = button(self.color): self.inner; + key_nav = true; + hover_highlight = true; }] pub struct Button { core: widget_core!(), @@ -114,13 +116,6 @@ impl_scope! { mgr.add_accel_keys(self.id_ref(), &self.keys1); } - fn key_nav(&self) -> bool { - true - } - fn hover_highlight(&self) -> bool { - true - } - fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { event.on_activate(mgr, self.id(), |mgr| { if let Some(f) = self.on_push.as_ref() { @@ -143,6 +138,8 @@ impl_scope! { #[derive(Clone)] #[widget { layout = button(self.color): self.label; + key_nav = true; + hover_highlight = true; }] pub struct TextButton { core: widget_core!(), @@ -249,13 +246,6 @@ impl_scope! { mgr.add_accel_keys(self.id_ref(), self.label.keys()); } - fn key_nav(&self) -> bool { - true - } - fn hover_highlight(&self) -> bool { - true - } - fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { event.on_activate(mgr, self.id(), |mgr| { if let Some(f) = self.on_push.as_ref() { diff --git a/crates/kas-widgets/src/combobox.rs b/crates/kas-widgets/src/combobox.rs index 1a4b2ef0f..05402d109 100644 --- a/crates/kas-widgets/src/combobox.rs +++ b/crates/kas-widgets/src/combobox.rs @@ -31,6 +31,8 @@ impl_scope! { #[derive(Clone)] #[widget { layout = button 'frame: row: [self.label, self.mark]; + key_nav = true; + hover_highlight = true; }] pub struct ComboBox { core: widget_core!(), @@ -52,14 +54,6 @@ impl_scope! { mgr.new_accel_layer(self.id(), true); } - fn key_nav(&self) -> bool { - true - } - - fn hover_highlight(&self) -> bool { - true - } - fn spatial_nav(&mut self, _: &mut SetRectMgr, _: bool, _: Option) -> Option { // We have no child within our rect None diff --git a/crates/kas-widgets/src/edit_field.rs b/crates/kas-widgets/src/edit_field.rs index a876ce337..58ccf14d3 100644 --- a/crates/kas-widgets/src/edit_field.rs +++ b/crates/kas-widgets/src/edit_field.rs @@ -376,7 +376,7 @@ impl_scope! { let class = TextClass::Edit(self.multi_line); draw.with_clip_region(self.rect(), self.view_offset, |mut draw| { if self.selection.is_empty() { - draw.text(self.rect().pos, self.text.as_ref(), class); + draw.text(self.rect().pos, &self.text, class); } else { // TODO(opt): we could cache the selection rectangles here to make // drawing more efficient (self.text.highlight_lines(range) output). @@ -391,7 +391,7 @@ impl_scope! { if self.editable && draw.ev_state().has_char_focus(self.id_ref()).0 { draw.text_cursor( self.rect().pos, - self.text.as_ref(), + &self.text, class, self.selection.edit_pos(), ); diff --git a/crates/kas-widgets/src/label.rs b/crates/kas-widgets/src/label.rs index 04bf3e612..59e9433a6 100644 --- a/crates/kas-widgets/src/label.rs +++ b/crates/kas-widgets/src/label.rs @@ -131,13 +131,13 @@ impl_scope! { #[cfg(feature = "min_spec")] impl<'a> Layout for Label<&'a str> { fn draw(&mut self, mut draw: DrawMgr) { - draw.text(self.rect().pos, self.label.as_ref(), self.class); + draw.text(self.rect().pos, &self.label, self.class); } } #[cfg(feature = "min_spec")] impl Layout for StringLabel { fn draw(&mut self, mut draw: DrawMgr) { - draw.text(self.rect().pos, self.label.as_ref(), self.class); + draw.text(self.rect().pos, &self.label, self.class); } } diff --git a/crates/kas-widgets/src/lib.rs b/crates/kas-widgets/src/lib.rs index 795feb9c0..9d1538284 100644 --- a/crates/kas-widgets/src/lib.rs +++ b/crates/kas-widgets/src/lib.rs @@ -79,6 +79,7 @@ mod scroll_label; mod scrollbar; mod separator; mod slider; +mod spinner; mod splitter; mod stack; @@ -96,7 +97,7 @@ pub use frame::{Frame, PopupFrame}; pub use grid::{BoxGrid, Grid}; pub use label::{AccelLabel, Label, StrLabel, StringLabel}; pub use list::*; -pub use mark::Mark; +pub use mark::{Mark, MarkButton}; pub use nav_frame::{NavFrame, SelectMsg}; pub use progress::ProgressBar; pub use radiobox::{RadioBox, RadioBoxBare, RadioBoxGroup}; @@ -105,5 +106,6 @@ pub use scroll_label::ScrollLabel; pub use scrollbar::{ScrollBar, ScrollBarRegion, ScrollBars, Scrollable}; pub use separator::Separator; pub use slider::{Slider, SliderType}; +pub use spinner::{Spinner, SpinnerType}; pub use splitter::*; pub use stack::{BoxStack, RefStack, Stack}; diff --git a/crates/kas-widgets/src/mark.rs b/crates/kas-widgets/src/mark.rs index 3e411f5f1..e89fc4d50 100644 --- a/crates/kas-widgets/src/mark.rs +++ b/crates/kas-widgets/src/mark.rs @@ -5,10 +5,9 @@ //! Mark widget -use kas::layout::{AxisInfo, SizeRules}; -use kas::theme::{DrawMgr, MarkStyle, SizeMgr}; -use kas::Layout; -use kas_macros::impl_scope; +use kas::prelude::*; +use kas::theme::MarkStyle; +use std::fmt::Debug; impl_scope! { /// A mark @@ -49,3 +48,50 @@ impl_scope! { } } } + +impl_scope! { + /// A mark which is also a button + /// + /// This button is not keyboard navigable; only mouse/touch interactive. + #[derive(Clone, Debug)] + #[widget { + hover_highlight = true; + }] + pub struct MarkButton { + core: widget_core!(), + style: MarkStyle, + msg: M, + } + + impl Self { + /// Construct + /// + /// A clone of `msg` is sent as a message on click. + pub fn new(style: MarkStyle, msg: M) -> Self { + MarkButton { + core: Default::default(), + style, + msg, + } + } + } + + impl Layout for Self { + fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + mgr.mark(self.style, axis) + } + + fn draw(&mut self, mut draw: DrawMgr) { + draw.mark(self.core.rect, self.style); + } + } + + impl Widget for Self { + fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { + event.on_activate(mgr, self.id(), |mgr| { + mgr.push_msg(self.msg.clone()); + Response::Used + }) + } + } +} diff --git a/crates/kas-widgets/src/menu/menu_entry.rs b/crates/kas-widgets/src/menu/menu_entry.rs index 99a0eab80..cd9bb31e8 100644 --- a/crates/kas-widgets/src/menu/menu_entry.rs +++ b/crates/kas-widgets/src/menu/menu_entry.rs @@ -21,6 +21,7 @@ impl_scope! { #[derive(Clone, Debug, Default)] #[widget { layout = self.label; + key_nav = true; }] pub struct MenuEntry { core: widget_core!(), @@ -78,10 +79,6 @@ impl_scope! { mgr.add_accel_keys(self.id_ref(), self.label.keys()); } - fn key_nav(&self) -> bool { - true - } - fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { match event { Event::Activate => { diff --git a/crates/kas-widgets/src/menu/submenu.rs b/crates/kas-widgets/src/menu/submenu.rs index 4c9ba7ce5..b8b75008a 100644 --- a/crates/kas-widgets/src/menu/submenu.rs +++ b/crates/kas-widgets/src/menu/submenu.rs @@ -134,7 +134,7 @@ impl_scope! { draw.frame(self.rect(), FrameStyle::MenuEntry, Default::default()); self.label.draw(draw.re_id(self.id())); if self.mark.rect().size != Size::ZERO { - self.mark.draw(draw); + draw.recurse(&mut self.mark); } } } diff --git a/crates/kas-widgets/src/radiobox.rs b/crates/kas-widgets/src/radiobox.rs index 556fb11af..49b9a5ad3 100644 --- a/crates/kas-widgets/src/radiobox.rs +++ b/crates/kas-widgets/src/radiobox.rs @@ -19,7 +19,10 @@ impl_scope! { /// A bare radiobox (no label) #[autoimpl(Debug ignore self.on_select)] #[derive(Clone)] - #[widget] + #[widget { + key_nav = true; + hover_highlight = true; + }] pub struct RadioBoxBare { core: widget_core!(), state: bool, @@ -32,13 +35,6 @@ impl_scope! { self.group.update_on_handles(mgr.ev_state(), self.id_ref()); } - fn key_nav(&self) -> bool { - true - } - fn hover_highlight(&self) -> bool { - true - } - fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { match event { Event::HandleUpdate { .. } => { diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index c614c5f8b..a4190214e 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -45,7 +45,7 @@ impl_scope! { let class = TextClass::LabelScroll; draw.with_clip_region(self.rect(), self.view_offset, |mut draw| { if self.selection.is_empty() { - draw.text(self.rect().pos, self.text.as_ref(), class); + draw.text(self.rect().pos, &self.text, class); } else { // TODO(opt): we could cache the selection rectangles here to make // drawing more efficient (self.text.highlight_lines(range) output). diff --git a/crates/kas-widgets/src/slider.rs b/crates/kas-widgets/src/slider.rs index 113c8766e..04c6470a1 100644 --- a/crates/kas-widgets/src/slider.rs +++ b/crates/kas-widgets/src/slider.rs @@ -6,7 +6,7 @@ //! `Slider` control use std::fmt::Debug; -use std::ops::{Add, Sub}; +use std::ops::{Add, RangeInclusive, Sub}; use std::time::Duration; use super::DragHandle; @@ -90,7 +90,7 @@ impl_scope! { /// # Messages /// /// On value change, pushes a value of type `T`. - #[derive(Clone, Debug, Default)] + #[derive(Clone, Debug)] #[widget{ key_nav = true; hover_highlight = true; @@ -99,7 +99,7 @@ impl_scope! { core: widget_core!(), direction: D, // Terminology assumes vertical orientation: - range: (T, T), + range: RangeInclusive, step: T, value: T, #[widget] @@ -109,35 +109,35 @@ impl_scope! { impl Self where D: Default { /// Construct a slider /// - /// Values vary between the given `min` and `max`. When keyboard navigation + /// Values vary within the given `range`. When keyboard navigation /// is used, arrow keys will increment the value by `step` and page up/down /// keys by `step * 16`. /// /// The initial value defaults to the range's /// lower bound but may be specified via [`Slider::with_value`]. #[inline] - pub fn new(min: T, max: T, step: T) -> Self { - Slider::new_with_direction(min, max, step, D::default()) + pub fn new(range: RangeInclusive, step: T) -> Self { + Slider::new_with_direction(range, step, D::default()) } } impl Self { /// Construct a slider with the given `direction` /// - /// Values vary between the given `min` and `max`. When keyboard navigation + /// Values vary within the given `range`. When keyboard navigation /// is used, arrow keys will increment the value by `step` and page up/down /// keys by `step * 16`. /// /// The initial value defaults to the range's /// lower bound but may be specified via [`Slider::with_value`]. #[inline] - pub fn new_with_direction(min: T, max: T, step: T, direction: D) -> Self { - assert!(min <= max); - let value = min; + pub fn new_with_direction(range: RangeInclusive, step: T, direction: D) -> Self { + assert!(!range.is_empty()); + let value = *range.start(); Slider { core: Default::default(), direction, - range: (min, max), + range, step, value, handle: DragHandle::new(), @@ -147,13 +147,8 @@ impl_scope! { /// Set the initial value #[inline] #[must_use] - pub fn with_value(mut self, mut value: T) -> Self { - if value < self.range.0 { - value = self.range.0; - } else if value > self.range.1 { - value = self.range.1; - } - self.value = value; + pub fn with_value(mut self, value: T) -> Self { + self.value = self.clamp_value(value); self } @@ -166,10 +161,10 @@ impl_scope! { #[inline] #[allow(clippy::neg_cmp_op_on_partial_ord)] fn clamp_value(&self, value: T) -> T { - if !(value >= self.range.0) { - self.range.0 - } else if !(value <= self.range.1) { - self.range.1 + if !(value >= *self.range.start()) { + *self.range.start() + } else if !(value <= *self.range.end()) { + *self.range.end() } else { value } @@ -190,8 +185,8 @@ impl_scope! { // translate value to offset in local coordinates fn offset(&self) -> Offset { - let a = self.value - self.range.0; - let b = self.range.1 - self.range.0; + let a = self.value - *self.range.start(); + let b = *self.range.end() - *self.range.start(); let max_offset = self.handle.max_offset(); let mut frac = a.div_as_f64(b); assert!((0.0..=1.0).contains(&frac)); @@ -205,7 +200,7 @@ impl_scope! { } fn set_offset_and_push_msg(&mut self, mgr: &mut EventMgr, offset: Offset) { - let b = self.range.1 - self.range.0; + let b = *self.range.end() - *self.range.start(); let max_offset = self.handle.max_offset(); let mut a = match self.direction.is_vertical() { false => b.mul_f64(offset.0 as f64 / max_offset.0 as f64), @@ -214,7 +209,7 @@ impl_scope! { if self.direction.is_reversed() { a = b - a; } - let value = self.clamp_value(a + self.range.0); + let value = self.clamp_value(a + *self.range.start()); if value != self.value { self.value = value; *mgr |= self.handle.set_offset(self.offset()).1; @@ -288,8 +283,8 @@ impl_scope! { true => self.value - x, } } - Command::Home => self.range.0, - Command::End => self.range.1, + Command::Home => *self.range.start(), + Command::End => *self.range.end(), _ => return Response::Unused, }; let action = self.set_value(v); diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs new file mode 100644 index 000000000..a42106ec8 --- /dev/null +++ b/crates/kas-widgets/src/spinner.rs @@ -0,0 +1,195 @@ +// 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 + +//! Spinner widget + +use crate::{EditBox, EditField, EditGuard, MarkButton}; +use kas::event::{Command, ScrollDelta}; +use kas::prelude::*; +use kas::theme::MarkStyle; +use std::ops::{Add, RangeInclusive, Sub}; + +/// Requirements on type used by [`Spinner`] +pub trait SpinnerType: + Copy + + Add + + Sub + + PartialOrd + + std::fmt::Debug + + std::str::FromStr + + ToString + + Sized + + 'static +{ +} +impl< + T: Copy + + Add + + Sub + + PartialOrd + + std::fmt::Debug + + std::str::FromStr + + ToString + + Sized + + 'static, + > SpinnerType for T +{ +} + +#[derive(Clone, Debug)] +enum SpinBtn { + Down, + Up, +} + +#[derive(Clone, Debug)] +struct SpinnerGuard(T, RangeInclusive); +impl SpinnerGuard { + #[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 + }; + } +} +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()); + edit.set_error_state(false); + } + mgr.push_msg(edit.guard.0); + } + + fn focus_lost(edit: &mut EditField, mgr: &mut EventMgr) { + Self::activate(edit, mgr); + } + + fn edit(edit: &mut EditField, _: &mut EventMgr) { + let is_err = match edit.get_str().parse() { + Ok(value) if edit.guard.1.contains(&value) => { + edit.guard.0 = value; + false + } + Ok(value) => { + edit.guard.set_value(value); + true + } + _ => true, + }; + edit.set_error_state(is_err); + } +} + +impl_scope! { + /// A numeric entry widget with up/down arrows + /// + /// Sends a message of type `T` on edit. + #[derive(Clone, Debug)] + #[widget { + layout = row: [ + self.edit, + align(center): column: [ + MarkButton::new(MarkStyle::Point(Direction::Up), SpinBtn::Up), + MarkButton::new(MarkStyle::Point(Direction::Down), SpinBtn::Down), + ], + ]; + }] + pub struct Spinner { + core: widget_core!(), + #[widget] + edit: EditBox>, + step: T, + } + + impl Self { + /// Construct + 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: EditBox::new(guard.0.to_string()).with_guard(guard), + step, + } + } + + /// Set the initial value + #[inline] + #[must_use] + pub fn with_value(mut self, value: T) -> Self { + self.edit.guard.set_value(value); + self + } + + /// Get the current value + #[inline] + pub fn value(&self) -> T { + self.edit.guard.0 + } + + /// 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()) + } + + fn set_and_emit(&mut self, mgr: &mut EventMgr, value: T) -> Response { + *mgr |= self.set_value(value); + mgr.push_msg(self.value()); + Response::Used + } + } + + impl Widget for Self { + fn steal_event(&mut self, mgr: &mut EventMgr, _: &WidgetId, event: &Event) -> Response { + match event { + Event::Command(cmd, _) => { + let value = match cmd { + Command::Down => self.value() - self.step, + Command::Up => self.value() + self.step, + _ => 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) + } else if *y < 0.0 { + self.set_and_emit(mgr, self.value() - self.step) + } else { + Response::Unused + } + } + _ => Response::Unused, + } + } + + 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()); + } + } + } +} diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index c1c8c1eed..e53c54388 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -10,12 +10,13 @@ use crate::{ CheckBoxBare, EditBox, EditField, EditGuard, Label, NavFrame, ProgressBar, RadioBoxGroup, - SliderType, + SliderType, SpinnerType, }; use kas::prelude::*; use std::default::Default; use std::fmt::Debug; use std::marker::PhantomData; +use std::ops::RangeInclusive; /// View widget driver/binder /// @@ -245,30 +246,27 @@ impl Driver for RadioBox { } /// [`crate::Slider`] view widget constructor -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct Slider { - min: T, - max: T, + range: RangeInclusive, step: T, direction: D, } impl Slider { - /// Construct, with given `min`, `max` and `step` (see [`crate::Slider::new`]) - pub fn make(min: T, max: T, step: T) -> Self { + /// Construct, with given `range` and `step` (see [`crate::Slider::new`]) + pub fn make(range: RangeInclusive, step: T) -> Self { Slider { - min, - max, + range, step, direction: D::default(), } } } impl Slider { - /// Construct, with given `min`, `max`, `step` and `direction` (see [`Slider::new_with_direction`]) - pub fn new_with_direction(min: T, max: T, step: T, direction: D) -> Self { + /// 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 { - min, - max, + range, step, direction, } @@ -277,7 +275,29 @@ impl Slider { impl Driver for Slider { type Widget = crate::Slider; fn make(&self) -> Self::Widget { - crate::Slider::new_with_direction(self.min, self.max, self.step, self.direction) + 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) + } +} + +/// [`crate::Spinner`] view widget constructor +#[derive(Clone, Debug)] +pub struct Spinner { + range: RangeInclusive, + step: T, +} +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 { + 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) diff --git a/crates/kas-widgets/src/view/mod.rs b/crates/kas-widgets/src/view/mod.rs index 09ff26212..011dd8e91 100644 --- a/crates/kas-widgets/src/view/mod.rs +++ b/crates/kas-widgets/src/view/mod.rs @@ -31,7 +31,6 @@ //! The user may implement a [`Driver`] or may use a standard one: //! //! - [`driver::DefaultView`] constructs a default view widget over various data types -//! - [`driver::DefaultEdit`] chooses a widget allowing editing of the shared data //! - [`driver::DefaultNav`] is a variant of the above, ensuring items support //! keyboard navigation (e.g. useful to allow selection of static items) //! - [`driver::CheckBox`] and [`driver::RadioBox`] support the `bool` type diff --git a/examples/async-event.rs b/examples/async-event.rs index 133fb272f..9603c47c5 100644 --- a/examples/async-event.rs +++ b/examples/async-event.rs @@ -13,6 +13,7 @@ use std::time::{Duration, Instant}; use kas::draw::color::Rgba; use kas::prelude::*; +use kas::theme::TextClass; fn main() -> kas::shell::Result<()> { env_logger::init(); @@ -33,11 +34,7 @@ fn main() -> kas::shell::Result<()> { thread::spawn(move || generate_colors(proxy, handle, colour2)); - let widget = ColourSquare { - core: Default::default(), - colour, - handle, - }; + let widget = ColourSquare::new(colour, handle); toolkit.with(widget)?.run() } @@ -49,15 +46,39 @@ impl_scope! { core: widget_core!(), colour: Arc>, handle: UpdateHandle, + loading_text: Text<&'static str>, + loaded: bool, + } + impl Self { + fn new(colour: Arc>, handle: UpdateHandle) -> Self { + ColourSquare { + core: Default::default(), + colour, + handle, + loading_text: Text::new_single("Loading..."), + loaded: false, + } + } } impl Layout for ColourSquare { fn size_rules(&mut self, mgr: SizeMgr, _: AxisInfo) -> SizeRules { SizeRules::fixed_scaled(100.0, 10.0, mgr.scale_factor()) } + + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { + self.core.rect = rect; + let align = align.unwrap_or(Align::Center, Align::Center); + mgr.text_set_size(&mut self.loading_text, TextClass::Label(false), rect.size, align); + } + fn draw(&mut self, mut draw: DrawMgr) { - let draw = draw.draw_device(); - let col = *self.colour.lock().unwrap(); - draw.rect((self.rect()).cast(), col); + if !self.loaded { + draw.text(self.core.rect.pos, &self.loading_text, TextClass::Label(false)); + } else { + let draw = draw.draw_device(); + let col = *self.colour.lock().unwrap(); + draw.rect((self.rect()).cast(), col); + } } } impl Widget for ColourSquare { @@ -70,6 +91,7 @@ impl_scope! { Event::HandleUpdate { .. } => { // Note: event has `handle` and `payload` params. // We only need to request a redraw. + self.loaded = true; mgr.redraw(self.id()); Response::Used } @@ -87,6 +109,9 @@ fn generate_colors( handle: UpdateHandle, colour: Arc>, ) { + // Loading takes time: + thread::sleep(Duration::from_secs(1)); + // This function is called in a separate thread, and runs until the program ends. let start_time = Instant::now(); diff --git a/examples/gallery.rs b/examples/gallery.rs index af2d358f4..f449dae3a 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -18,14 +18,14 @@ use kas::widgets::{menu::MenuEntry, view::SingleView, *}; #[derive(Clone, Debug)] enum Item { Button, - LightTheme, - DarkTheme, + Theme(&'static str), Check(bool), Combo(i32), Radio(u32), Edit(String), Slider(i32), Scroll(i32), + Spinner(i32), } #[derive(Debug)] @@ -159,6 +159,7 @@ fn main() -> Result<(), Box> { row: ["RadioBox", self.rb], row: ["RadioBox", self.rb2], row: ["ComboBox", self.cbb], + row: ["Spinner", self.spin], row: ["Slider", self.sd], row: ["ScrollBar", self.sc], row: ["ProgressBar", self.pg], @@ -173,11 +174,14 @@ fn main() -> Result<(), Box> { #[widget] eb = EditBox::new("edit me").with_guard(Guard), #[widget] tb = TextButton::new_msg("&Press me", Item::Button), #[widget] bi = row![ - Button::new_msg(img_light, Item::LightTheme) - .with_color("#FAFAFA".parse().unwrap()) + Button::new_msg(img_light.clone(), Item::Theme("light")) + .with_color("#B38DF9".parse().unwrap()) .with_keys(&[VK::L]), - Button::new_msg(img_dark, Item::DarkTheme) - .with_color("#404040".parse().unwrap()) + Button::new_msg(img_light, Item::Theme("blue")) + .with_color("#7CDAFF".parse().unwrap()) + .with_keys(&[VK::L]), + Button::new_msg(img_dark, Item::Theme("dark")) + .with_color("#E77346".parse().unwrap()) .with_keys(&[VK::K]), ], #[widget] cb = CheckBox::new("&Check me") @@ -193,9 +197,8 @@ fn main() -> Result<(), Box> { MenuEntry::new("T&wo", Item::Combo(2)), MenuEntry::new("Th&ree", Item::Combo(3)), ]), - #[widget] sd = Slider::::new(0, 10, 1) - .with_value(0) - .map_msg(|msg: i32| Item::Slider(msg)), + #[widget] spin: Spinner = Spinner::new(0..=10, 1), + #[widget] sd: Slider = Slider::new(0..=10, 1), #[widget] sc: ScrollBar = ScrollBar::new().with_limits(100, 20), #[widget] pg: ProgressBar = ProgressBar::new(), #[widget] sv = img_rustacean.with_scaling(|s| { @@ -207,8 +210,14 @@ fn main() -> Result<(), Box> { } impl Widget for Self { fn handle_message(&mut self, mgr: &mut EventMgr, index: usize) { - if index == widget_index![self.sc] { - if let Some(msg) = mgr.try_pop_msg::() { + 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] { let ratio = msg as f32 / self.sc.max_value() as f32; *mgr |= self.pg.set_value(ratio); mgr.push_msg(Item::Scroll(msg)) @@ -258,16 +267,10 @@ fn main() -> Result<(), Box> { } } } else if let Some(item) = mgr.try_pop_msg::() { + println!("Message: {item:?}"); match item { - Item::Button => println!("Clicked!"), - Item::LightTheme => mgr.adjust_theme(|theme| theme.set_scheme("light")), - Item::DarkTheme => mgr.adjust_theme(|theme| theme.set_scheme("dark")), - Item::Check(b) => println!("CheckBox: {}", b), - Item::Combo(c) => println!("ComboBox: {}", c), - Item::Radio(id) => println!("RadioBox: {}", id), - Item::Edit(s) => println!("Edited: {}", s), - Item::Slider(p) => println!("Slider: {}", p), - Item::Scroll(p) => println!("ScrollBar: {}", p), + Item::Theme(name) => mgr.adjust_theme(|theme| theme.set_scheme(name)), + _ => (), } } } diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs index 1d335b2a8..791269b20 100644 --- a/examples/mandlebrot/mandlebrot.rs +++ b/examples/mandlebrot/mandlebrot.rs @@ -446,7 +446,7 @@ 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); let mbrot = Mandlebrot::new(); MandlebrotWindow { core: Default::default(),