diff --git a/crates/kas-core/src/component.rs b/crates/kas-core/src/component.rs new file mode 100644 index 000000000..2061c6a98 --- /dev/null +++ b/crates/kas-core/src/component.rs @@ -0,0 +1,175 @@ +// 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 + +//! Widget components + +use crate::geom::{Coord, Rect, Size}; +use crate::layout::{Align, AlignHints, AxisInfo, SetRectMgr, SizeRules}; +use crate::text::{format, AccelString, Text, TextApi}; +use crate::theme::{DrawMgr, IdCoord, IdRect, MarkStyle, SizeMgr, TextClass}; +use crate::{TkAction, WidgetId}; +use kas_macros::{autoimpl, impl_scope}; + +/// Components are not true widgets, but support layout solving +/// +/// TODO: since this is a sub-set of widget functionality, should [`crate::Widget`] +/// extend `Component`? (Significant trait revision would be required.) +pub trait Component { + /// Get size rules for the given axis + fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules; + + /// Apply a given `rect` to self + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints); + + /// True if the layout direction is up/left (reverse reading direction) + /// + /// TODO: replace with spatial_nav? + fn is_reversed(&self) -> bool { + false + } + + /// Translate a coordinate to a [`WidgetId`] + fn find_id(&mut self, coord: Coord) -> Option; + + /// Draw the component and its children + fn draw(&mut self, draw: DrawMgr, id: &WidgetId); +} + +impl_scope! { + /// A label component + #[impl_default(where T: trait)] + #[autoimpl(Clone, Debug where T: trait)] + pub struct Label { + text: Text, + class: TextClass = TextClass::Label(false), + pos: Coord, + } + + impl Self { + /// Construct + #[inline] + pub fn new(label: T, class: TextClass) -> Self { + Label { + text: Text::new_single(label), + class, + pos: Default::default(), + } + } + + /// Get text + pub fn as_str(&self) -> &str { + self.text.as_str() + } + + /// Set the text and prepare + /// + /// Update text and trigger a resize if necessary. + /// + /// The `avail` parameter is used to determine when a resize is required. If + /// this parameter is a little bit wrong then resizes may sometimes happen + /// unnecessarily or may not happen when text is slightly too big (e.g. + /// spills into the margin area); this behaviour is probably acceptable. + /// Passing `Size::ZERO` will always resize (unless text is empty). + /// Passing `Size::MAX` should never resize. + pub fn set_text_and_prepare(&mut self, s: T, avail: Size) -> TkAction { + self.text.set_text(s); + crate::text::util::prepare_if_needed(&mut self.text, avail) + } + + /// Set the text from a string and prepare + /// + /// Update text and trigger a resize if necessary. + /// + /// The `avail` parameter is used to determine when a resize is required. If + /// this parameter is a little bit wrong then resizes may sometimes happen + /// unnecessarily or may not happen when text is slightly too big (e.g. + /// spills into the margin area); this behaviour is probably acceptable. + pub fn set_string_and_prepare(&mut self, s: String, avail: Size) -> TkAction + where + T: format::EditableText, + { + use crate::text::EditableTextApi; + self.text.set_string(s); + crate::text::util::prepare_if_needed(&mut self.text, avail) + } + + /// Get class + pub fn class(&self) -> TextClass { + self.class + } + + /// Set class + /// + /// This may influence layout. + pub fn set_class(&mut self, class: TextClass) -> TkAction { + self.class = class; + TkAction::RESIZE + } + } + + impl Label { + /// Get the accelerator keys + pub fn keys(&self) -> &[crate::event::VirtualKeyCode] { + self.text.text().keys() + } + } + + impl Component for Self { + fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + mgr.text_bound(&mut self.text, self.class, axis) + } + + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { + self.pos = rect.pos; + let halign = match self.class { + TextClass::Button => Align::Center, + _ => Align::Default, + }; + let align = align.unwrap_or(halign, Align::Center); + mgr.text_set_size(&mut self.text, self.class, rect.size, align); + } + + fn find_id(&mut self, _: Coord) -> Option { + None + } + + fn draw(&mut self, mut draw: DrawMgr, id: &WidgetId) { + draw.text_effects(IdCoord(id, self.pos), &self.text, self.class); + } + } +} + +impl_scope! { + /// A mark + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct Mark { + pub style: MarkStyle, + pub rect: Rect, + } + impl Self { + /// Construct + pub fn new(style: MarkStyle) -> Self { + let rect = Rect::ZERO; + Mark { style, rect } + } + } + impl Component for Self { + fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + mgr.mark(self.style, axis) + } + + fn set_rect(&mut self, _: &mut SetRectMgr, rect: Rect, _: AlignHints) { + self.rect = rect; + } + + fn find_id(&mut self, _: Coord) -> Option { + None + } + + fn draw(&mut self, mut draw: DrawMgr, id: &WidgetId) { + draw.mark(IdRect(id, self.rect), self.style); + } + } +} diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs index c4c842ea4..0951176c6 100644 --- a/crates/kas-core/src/core/widget.rs +++ b/crates/kas-core/src/core/widget.rs @@ -132,8 +132,9 @@ pub trait WidgetChildren: WidgetCore { /// Widgets are *configured* on window creation or dynamically via the /// parent calling [`SetRectMgr::configure`]. Parent widgets are responsible /// for ensuring that children are configured before calling -/// [`Layout::size_rules`]. Configuration may be repeated and may be used as a -/// mechanism to change a child's [`WidgetId`], but this may be expensive. +/// [`Layout::size_rules`] or [`Layout::set_rect`]. Configuration may be +/// repeated and may be used as a mechanism to change a child's [`WidgetId`], +/// but this may be expensive. /// /// Configuration invokes [`Self::configure_recurse`] which then calls /// [`Self::configure`]. The latter may be used to load assets before sizing. @@ -229,10 +230,13 @@ pub trait WidgetConfig: Layout { /// methods may be required (e.g. [`Self::set_rect`] to position child /// elements). /// -/// Layout solving happens in two steps: +/// Two methods of setting layout are possible: /// -/// 1. [`Self::size_rules`] calculates size requirements recursively -/// 2. [`Self::set_rect`] applies the result recursively +/// 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. /// /// [`derive(Widget)`]: https://docs.rs/kas/latest/kas/macros/index.html#the-derivewidget-macro #[autoimpl(for Box)] @@ -258,12 +262,11 @@ pub trait Layout: WidgetChildren { /// /// Typically, this method is called twice: first for the horizontal axis, /// second for the vertical axis (with resolved width available through - /// the `axis` parameter allowing content wrapping). On re-sizing, the - /// first or both method calls may be skipped. + /// the `axis` parameter allowing content wrapping). /// - /// This method takes `&mut self` since it may be necessary to store child - /// element size rules in order to calculate layout by `size_rules` on the - /// second axis and by `set_rect`. + /// When called, this method should cache any data required to determine + /// internal layout (of child widgets and other components), especially data + /// which requires calling `size_rules` on children. /// /// This method may be implemented through [`Self::layout`] or directly. /// A [`crate::layout::RulesSolver`] engine may be useful to calculate @@ -275,25 +278,25 @@ pub trait Layout: WidgetChildren { self.layout().size_rules(size_mgr, axis) } - /// Apply a given `rect` to self + /// Set size and position /// - /// This method applies the layout resolved by [`Self::size_rules`]. + /// This is the final step to layout solving. It may be influenced by + /// [`Self::size_rules`], but it is not guaranteed that `size_rules` is + /// called first. After calling `set_rect`, the widget must be ready for + /// calls to [`Self::draw`] and event handling. /// - /// This method may be implemented through [`Self::layout`] or directly. - /// For widgets without children, typically this method only stores the - /// calculated `rect`, which is done by the default implementation (even - /// with the default empty layout for [`Self::layout`]). - /// - /// This method may also be useful for alignment, which may be applied in - /// one of two ways: + /// The size of the assigned `rect` is normally at least the minimum size + /// requested by [`Self::size_rules`], but this is not guaranteed. In case + /// this minimum is not met, it is permissible for the widget to draw + /// outside of its assigned `rect` and to not function as normal. /// - /// 1. Shrinking `rect` to the "ideal size" and aligning within (see - /// [`crate::layout::CompleteAlignment::aligned_rect`] or example usage in - /// `CheckBoxBare` widget) - /// 2. Applying alignment to contents (see for example `Label` widget) + /// The assigned `rect` may be larger than the widget's size requirements. + /// It is up to the widget to either stretch to occupy this space or align + /// itself within the excess space, according to the `align` hints provided. /// - /// One may assume that `size_rules` has been called at least once for each - /// axis with current size information before this method. + /// This method may be implemented through [`Self::layout`] or directly. + /// The default implementation assigns `self.core_data_mut().rect = rect` + /// and applies the layout described by [`Self::layout`]. fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { self.core_data_mut().rect = rect; self.layout().set_rect(mgr, rect, align); @@ -360,6 +363,9 @@ pub trait Layout: WidgetChildren { /// inner content, while the `CheckBox` widget forwards click events to its /// `CheckBoxBare` component. /// + /// It is expected that [`Self::set_rect`] is called before this method, + /// but failure to do so should not cause a fatal error. + /// /// The default implementation suffices unless: /// /// - [`Self::layout`] is not implemented and there are child widgets @@ -387,6 +393,9 @@ pub trait Layout: WidgetChildren { /// This method is invoked each frame to draw visible widgets. It should /// draw itself and recurse into all visible children. /// + /// It is expected that [`Self::set_rect`] is called before this method, + /// but failure to do so should not cause a fatal error. + /// /// The default impl draws elements as defined by [`Self::layout`]. fn draw(&mut self, draw: DrawMgr) { let id = self.id(); // clone to avoid borrow conflict diff --git a/crates/kas-core/src/layout/grid_solver.rs b/crates/kas-core/src/layout/grid_solver.rs index eb93b0e9a..d97c18b9b 100644 --- a/crates/kas-core/src/layout/grid_solver.rs +++ b/crates/kas-core/src/layout/grid_solver.rs @@ -12,8 +12,9 @@ use super::{GridStorage, RowTemp, RulesSetter, RulesSolver}; use crate::cast::{Cast, Conv}; use crate::geom::{Coord, Offset, Rect, Size}; +/// Bound on [`GridSolver`] type parameters pub trait DefaultWithLen { - // Construct with default elements of given length; panic on failure + /// Construct with default elements of given length; panic on failure fn default_with_len(len: usize) -> Self; } impl DefaultWithLen for [T; N] { @@ -105,11 +106,11 @@ impl GridSolver GridSetter { if cols > 0 { let align = align.horiz.unwrap_or(Align::Default); - let (widths, rules, total) = storage.widths_rules_total(); + let (widths, rules) = storage.widths_and_rules(); + let total = SizeRules::sum(rules); let max_size = total.max_size(); let mut target = rect.size.0; @@ -312,7 +310,8 @@ impl GridSetter { if rows > 0 { let align = align.vert.unwrap_or(Align::Default); - let (heights, rules, total) = storage.heights_rules_total(); + let (heights, rules) = storage.heights_and_rules(); + let total = SizeRules::sum(rules); let max_size = total.max_size(); let mut target = rect.size.1; diff --git a/crates/kas-core/src/layout/mod.rs b/crates/kas-core/src/layout/mod.rs index 50d1a93bf..190e4848d 100644 --- a/crates/kas-core/src/layout/mod.rs +++ b/crates/kas-core/src/layout/mod.rs @@ -48,7 +48,9 @@ mod visitor; use crate::dir::{Direction, Directional}; use crate::draw::DrawShared; use crate::event::EventState; -use crate::theme::{SizeHandle, SizeMgr}; +use crate::geom::{Size, Vec2}; +use crate::text::TextApi; +use crate::theme::{SizeHandle, SizeMgr, TextClass}; use crate::{TkAction, WidgetConfig, WidgetId}; use std::ops::{Deref, DerefMut}; @@ -60,7 +62,7 @@ pub use size_rules::SizeRules; pub use size_types::*; pub use sizer::{solve_size_rules, RulesSetter, RulesSolver, SolveCache}; pub use storage::*; -pub use visitor::{FrameStorage, Layout, StorageChain, TextStorage}; +pub use visitor::{FrameStorage, Layout, StorageChain}; /// Information on which axis is being resized /// @@ -197,6 +199,20 @@ impl<'a> SetRectMgr<'a> { // future, hence do not advise calling `configure_recurse` directly. widget.configure_recurse(self, id); } + + /// Update a text object, setting font properties and wrap size + /// + /// Returns required size. + #[inline] + pub fn text_set_size( + &self, + text: &mut dyn TextApi, + class: TextClass, + size: Size, + align: (Align, Align), + ) -> Vec2 { + self.sh.text_set_size(text, class, size, align) + } } impl<'a> std::ops::BitOrAssign for SetRectMgr<'a> { diff --git a/crates/kas-core/src/layout/row_solver.rs b/crates/kas-core/src/layout/row_solver.rs index 279d3269c..0d271aa24 100644 --- a/crates/kas-core/src/layout/row_solver.rs +++ b/crates/kas-core/src/layout/row_solver.rs @@ -42,8 +42,8 @@ impl RowSolver { let axis_is_vertical = axis.is_vertical() ^ dir.is_vertical(); if axis.has_fixed && axis_is_vertical { - let (widths, rules, total) = storage.widths_rules_total(); - SizeRules::solve_seq_total(widths, rules, total, axis.other_axis); + let (widths, rules) = storage.widths_and_rules(); + SizeRules::solve_seq(widths, rules, axis.other_axis); } RowSolver { @@ -90,13 +90,8 @@ impl RulesSolver for RowSolver { } } - fn finish(self, storage: &mut Self::Storage) -> SizeRules { - let rules = self.rules.unwrap_or(SizeRules::EMPTY); - if !self.axis_is_vertical { - storage.set_total(rules); - } - - rules + fn finish(self, _: &mut Self::Storage) -> SizeRules { + self.rules.unwrap_or(SizeRules::EMPTY) } } @@ -136,7 +131,8 @@ impl RowSetter { if len > 0 { let is_horiz = direction.is_horizontal(); let mut width = if is_horiz { rect.size.0 } else { rect.size.1 }; - let (widths, rules, total) = storage.widths_rules_total(); + let (widths, rules) = storage.widths_and_rules(); + let total = SizeRules::sum(rules); let max_size = total.max_size(); let align = if is_horiz { align.horiz } else { align.vert }; let align = align.unwrap_or(Align::Default); @@ -225,7 +221,7 @@ impl RowSetter { pub fn solve_range(&mut self, storage: &mut S, range: Range, width: i32) { assert!(range.end <= self.offsets.as_mut().len()); - let (widths, rules, _) = storage.widths_rules_total(); + let (widths, rules) = storage.widths_and_rules(); SizeRules::solve_seq(&mut widths[range.clone()], &rules[range], width); } } diff --git a/crates/kas-core/src/layout/size_types.rs b/crates/kas-core/src/layout/size_types.rs index 43912620d..fb8fdb2cb 100644 --- a/crates/kas-core/src/layout/size_types.rs +++ b/crates/kas-core/src/layout/size_types.rs @@ -274,8 +274,6 @@ impl SpriteDisplay { } /// Generates `size_rules` based on size - /// - /// Set [`Self::size`] before calling this. pub fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo, raw_size: Size) -> SizeRules { let margins = self.margins.select(mgr.re()).extract(axis); let scale_factor = mgr.scale_factor(); diff --git a/crates/kas-core/src/layout/sizer.rs b/crates/kas-core/src/layout/sizer.rs index 7d0d9488b..3ce9e4d7b 100644 --- a/crates/kas-core/src/layout/sizer.rs +++ b/crates/kas-core/src/layout/sizer.rs @@ -62,16 +62,13 @@ pub trait RulesSetter { /// Solve size rules for a widget /// -/// It is required that a widget's `size_rules` method is called, for each axis, -/// before `set_rect`. This method may be used to call `size_rules` in case the -/// parent's own `size_rules` method may not. +/// Automatic layout solving requires that a widget's `size_rules` method is +/// called for each axis before `set_rect`. This method simply calls +/// `size_rules` on each axis. /// -/// Note: it is not necessary to solve size rules again before calling -/// `set_rect` a second time, although if the widget's content changes then it -/// is recommended. -/// -/// Note: it is not necessary to ever call `size_rules` *or* `set_rect` if the -/// widget is never drawn and never receives events. +/// If `size_rules` is not called, internal layout may be poor (depending on the +/// widget). If widget content changes, it is recommended to call +/// `solve_size_rules` and `set_rect` again. /// /// Parameters `x_size` and `y_size` should be passed where this dimension is /// fixed and are used e.g. for text wrapping. @@ -196,8 +193,8 @@ impl SolveCache { width -= self.margins.sum_horiz(); } - // We call size_rules not because we want the result, but because our - // spec requires that we do so before calling set_rect. + // We call size_rules not because we want the result, but to allow + // internal layout solving. if self.refresh_rules || width != self.last_width { if self.refresh_rules { let w = widget.size_rules(mgr.size_mgr(), AxisInfo::new(false, None)); diff --git a/crates/kas-core/src/layout/storage.rs b/crates/kas-core/src/layout/storage.rs index 815e52afc..061f3b098 100644 --- a/crates/kas-core/src/layout/storage.rs +++ b/crates/kas-core/src/layout/storage.rs @@ -32,26 +32,36 @@ impl Storage for () { /// Requirements of row solver storage type /// -/// Details are hidden (for internal use only). +/// Usually this is set by a [`crate::layout::RowSolver`] from +/// [`crate::Layout::size_rules`], then used by [`crate::Layout::set_rect`] to +/// divide the assigned rect between children. +/// +/// It may be useful to access this directly if not solving size rules normally; +/// specifically this allows a different size solver to replace `size_rules` and +/// influence `set_rect`. +/// +/// Note: some implementations allocate when [`Self::set_dim`] is first called. +/// It is expected that this method is called before other methods. pub trait RowStorage: sealed::Sealed + Clone { - #[doc(hidden)] + /// Set dimension: number of columns or rows fn set_dim(&mut self, cols: usize); - #[doc(hidden)] + /// Access [`SizeRules`] for each column/row fn rules(&mut self) -> &mut [SizeRules] { - self.widths_rules_total().1 + self.widths_and_rules().1 } - #[doc(hidden)] - fn set_total(&mut self, total: SizeRules); - - #[doc(hidden)] + /// Access widths for each column/row + /// + /// Widths are calculated from rules when `set_rect` is called. Assigning + /// to widths before `set_rect` is called only has any effect when the available + /// size exceeds the minimum required (see [`SizeRules::solve_seq`]). fn widths(&mut self) -> &mut [i32] { - self.widths_rules_total().0 + self.widths_and_rules().0 } - #[doc(hidden)] - fn widths_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules); + /// Access widths and rules simultaneously + fn widths_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]); } /// Fixed-length row storage @@ -60,7 +70,6 @@ pub trait RowStorage: sealed::Sealed + Clone { #[derive(Clone, Debug)] pub struct FixedRowStorage { rules: [SizeRules; C], - total: SizeRules, widths: [i32; C], } @@ -68,7 +77,6 @@ impl Default for FixedRowStorage { fn default() -> Self { FixedRowStorage { rules: [SizeRules::default(); C], - total: SizeRules::default(), widths: [0; C], } } @@ -86,12 +94,8 @@ impl RowStorage for FixedRowStorage { assert_eq!(self.widths.as_ref().len(), cols); } - fn set_total(&mut self, total: SizeRules) { - self.total = total; - } - - fn widths_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules) { - (self.widths.as_mut(), self.rules.as_mut(), self.total) + fn widths_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]) { + (self.widths.as_mut(), self.rules.as_mut()) } } @@ -99,7 +103,6 @@ impl RowStorage for FixedRowStorage { #[derive(Clone, Debug, Default)] pub struct DynRowStorage { rules: Vec, - total: SizeRules, widths: Vec, } @@ -115,12 +118,8 @@ impl RowStorage for DynRowStorage { self.widths.resize(cols, 0); } - fn set_total(&mut self, total: SizeRules) { - self.total = total; - } - - fn widths_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules) { - (&mut self.widths, &mut self.rules, self.total) + fn widths_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]) { + (&mut self.widths, &mut self.rules) } } @@ -150,38 +149,53 @@ where /// Requirements of grid solver storage type /// -/// Details are hidden (for internal use only). +/// Usually this is set by a [`crate::layout::GridSolver`] from +/// [`crate::Layout::size_rules`], then used by [`crate::Layout::set_rect`] to +/// divide the assigned rect between children. +/// +/// It may be useful to access this directly if not solving size rules normally; +/// specifically this allows a different size solver to replace `size_rules` and +/// influence `set_rect`. +/// +/// Note: some implementations allocate when [`Self::set_dims`] is first called. +/// It is expected that this method is called before other methods. pub trait GridStorage: sealed::Sealed + Clone { - #[doc(hidden)] + /// Set dimension: number of columns and rows fn set_dims(&mut self, cols: usize, rows: usize); - #[doc(hidden)] + /// Access [`SizeRules`] for each column fn width_rules(&mut self) -> &mut [SizeRules] { - self.widths_rules_total().1 + self.widths_and_rules().1 } - #[doc(hidden)] + + /// Access [`SizeRules`] for each row fn height_rules(&mut self) -> &mut [SizeRules] { - self.heights_rules_total().1 + self.heights_and_rules().1 } - #[doc(hidden)] - fn set_width_total(&mut self, total: SizeRules); - #[doc(hidden)] - fn set_height_total(&mut self, total: SizeRules); - - #[doc(hidden)] + /// Access widths for each column + /// + /// Widths are calculated from rules when `set_rect` is called. Assigning + /// to widths before `set_rect` is called only has any effect when the available + /// size exceeds the minimum required (see [`SizeRules::solve_seq`]). fn widths(&mut self) -> &mut [i32] { - self.widths_rules_total().0 + self.widths_and_rules().0 } - #[doc(hidden)] + + /// Access heights for each row + /// + /// Heights are calculated from rules when `set_rect` is called. Assigning + /// to heights before `set_rect` is called only has any effect when the available + /// size exceeds the minimum required (see [`SizeRules::solve_seq`]). fn heights(&mut self) -> &mut [i32] { - self.heights_rules_total().0 + self.heights_and_rules().0 } - #[doc(hidden)] - fn widths_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules); - #[doc(hidden)] - fn heights_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules); + /// Access column widths and rules simultaneously + fn widths_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]); + + /// Access row heights and rules simultaneously + fn heights_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]); } impl_scope! { @@ -193,8 +207,6 @@ impl_scope! { pub struct FixedGridStorage { width_rules: [SizeRules; C] = [SizeRules::default(); C], height_rules: [SizeRules; R] = [SizeRules::default(); R], - width_total: SizeRules, - height_total: SizeRules, widths: [i32; C] = [0; C], heights: [i32; R] = [0; R], } @@ -214,28 +226,17 @@ impl_scope! { } #[doc(hidden)] - fn set_width_total(&mut self, total: SizeRules) { - self.width_total = total; - } - #[doc(hidden)] - fn set_height_total(&mut self, total: SizeRules) { - self.height_total = total; - } - - #[doc(hidden)] - fn widths_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules) { + fn widths_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]) { ( self.widths.as_mut(), self.width_rules.as_mut(), - self.width_total, ) } #[doc(hidden)] - fn heights_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules) { + fn heights_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]) { ( self.heights.as_mut(), self.height_rules.as_mut(), - self.height_total, ) } } @@ -246,8 +247,6 @@ impl_scope! { pub struct DynGridStorage { width_rules: Vec, height_rules: Vec, - width_total: SizeRules, - height_total: SizeRules, widths: Vec, heights: Vec, } @@ -267,29 +266,12 @@ impl GridStorage for DynGridStorage { } #[doc(hidden)] - fn set_width_total(&mut self, total: SizeRules) { - self.width_total = total; - } - #[doc(hidden)] - fn set_height_total(&mut self, total: SizeRules) { - self.height_total = total; - } - - #[doc(hidden)] - fn widths_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules) { - ( - self.widths.as_mut(), - self.width_rules.as_mut(), - self.width_total, - ) + fn widths_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]) { + (self.widths.as_mut(), self.width_rules.as_mut()) } #[doc(hidden)] - fn heights_rules_total(&mut self) -> (&mut [i32], &mut [SizeRules], SizeRules) { - ( - self.heights.as_mut(), - self.height_rules.as_mut(), - self.height_total, - ) + fn heights_and_rules(&mut self) -> (&mut [i32], &mut [SizeRules]) { + (self.heights.as_mut(), self.height_rules.as_mut()) } } diff --git a/crates/kas-core/src/layout/visitor.rs b/crates/kas-core/src/layout/visitor.rs index 6b6488438..6e91b90fa 100644 --- a/crates/kas-core/src/layout/visitor.rs +++ b/crates/kas-core/src/layout/visitor.rs @@ -11,11 +11,10 @@ use super::{AlignHints, AxisInfo, RulesSetter, RulesSolver, SetRectMgr, SizeRules, Storage}; use super::{DynRowStorage, RowPositionSolver, RowSetter, RowSolver, RowStorage}; use super::{GridChildInfo, GridDimensions, GridSetter, GridSolver, GridStorage}; -use crate::cast::Cast; +use crate::component::Component; use crate::draw::color::Rgb; use crate::geom::{Coord, Offset, Rect, Size}; -use crate::text::{Align, TextApi, TextApiExt}; -use crate::theme::{Background, DrawMgr, FrameStyle, IdCoord, IdRect, SizeMgr, TextClass}; +use crate::theme::{Background, DrawMgr, FrameStyle, IdRect, SizeMgr}; use crate::WidgetId; use crate::{dir::Directional, WidgetConfig}; use std::any::Any; @@ -55,21 +54,6 @@ impl StorageChain { } } -/// Implementation helper for layout of children -trait Visitor { - /// Get size rules for the given axis - fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules; - - /// Apply a given `rect` to self - fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints); - - fn is_reversed(&mut self) -> bool; - - fn find_id(&mut self, coord: Coord) -> Option; - - fn draw(&mut self, draw: DrawMgr, id: &WidgetId); -} - /// A layout visitor /// /// This constitutes a "visitor" which iterates over each child widget. Layout @@ -82,6 +66,10 @@ pub struct Layout<'a> { enum LayoutType<'a> { /// No layout None, + /// A component + Component(&'a mut dyn Component), + /// A boxed component + BoxComponent(Box), /// A single child widget Single(&'a mut dyn WidgetConfig), /// A single child widget with alignment @@ -92,8 +80,6 @@ enum LayoutType<'a> { Frame(Box>, &'a mut FrameStorage, FrameStyle), /// Button frame around content Button(Box>, &'a mut FrameStorage, Option), - /// An embedded layout - Visitor(Box), } impl<'a> Default for Layout<'a> { @@ -144,9 +130,9 @@ impl<'a> Layout<'a> { Layout { layout } } - /// Place a text element in the layout - pub fn text(data: &'a mut TextStorage, text: &'a mut dyn TextApi, class: TextClass) -> Self { - let layout = LayoutType::Visitor(Box::new(Text { data, text, class })); + /// Place a component in the layout + pub fn component(component: &'a mut dyn Component) -> Self { + let layout = LayoutType::Component(component); Layout { layout } } @@ -157,7 +143,7 @@ impl<'a> Layout<'a> { D: Directional, S: RowStorage, { - let layout = LayoutType::Visitor(Box::new(List { + let layout = LayoutType::BoxComponent(Box::new(List { data, direction, children: list, @@ -176,7 +162,7 @@ impl<'a> Layout<'a> { W: WidgetConfig, D: Directional, { - let layout = LayoutType::Visitor(Box::new(Slice { + let layout = LayoutType::BoxComponent(Box::new(Slice { data, direction, children: slice, @@ -190,7 +176,7 @@ impl<'a> Layout<'a> { I: Iterator)> + 'a, S: GridStorage, { - let layout = LayoutType::Visitor(Box::new(Grid { + let layout = LayoutType::BoxComponent(Box::new(Grid { data, dim, children: iter, @@ -205,7 +191,7 @@ impl<'a> Layout<'a> { } fn size_rules_(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { let frame = |mgr: SizeMgr, child: &mut Layout, storage: &mut FrameStorage, mut style| { - let frame_rules = mgr.frame(style, axis.is_vertical()); + let frame_rules = mgr.frame(style, axis); let child_rules = child.size_rules_(mgr, axis); if axis.is_horizontal() && style == FrameStyle::MenuEntry { style = FrameStyle::InnerMargin; @@ -223,12 +209,13 @@ impl<'a> Layout<'a> { }; match &mut self.layout { LayoutType::None => SizeRules::EMPTY, + LayoutType::Component(component) => component.size_rules(mgr, axis), + LayoutType::BoxComponent(component) => component.size_rules(mgr, axis), LayoutType::Single(child) => child.size_rules(mgr, axis), LayoutType::AlignSingle(child, _) => child.size_rules(mgr, axis), LayoutType::AlignLayout(layout, _) => layout.size_rules_(mgr, axis), LayoutType::Frame(child, storage, style) => frame(mgr, child, storage, *style), LayoutType::Button(child, storage, _) => frame(mgr, child, storage, FrameStyle::Button), - LayoutType::Visitor(visitor) => visitor.size_rules(mgr, axis), } } @@ -240,6 +227,8 @@ impl<'a> Layout<'a> { fn set_rect_(&mut self, mgr: &mut SetRectMgr, mut rect: Rect, align: AlignHints) { match &mut self.layout { LayoutType::None => (), + LayoutType::Component(component) => component.set_rect(mgr, rect, align), + LayoutType::BoxComponent(layout) => layout.set_rect(mgr, rect, align), LayoutType::Single(child) => child.set_rect(mgr, rect, align), LayoutType::AlignSingle(child, hints) => { let align = hints.combine(align); @@ -255,7 +244,6 @@ impl<'a> Layout<'a> { rect.size -= storage.size; child.set_rect_(mgr, rect, align); } - LayoutType::Visitor(layout) => layout.set_rect(mgr, rect, align), } } @@ -269,11 +257,12 @@ impl<'a> Layout<'a> { fn is_reversed_(&mut self) -> bool { match &mut self.layout { LayoutType::None => false, + LayoutType::Component(component) => component.is_reversed(), + LayoutType::BoxComponent(layout) => layout.is_reversed(), LayoutType::Single(_) | LayoutType::AlignSingle(_, _) => false, LayoutType::AlignLayout(layout, _) => layout.is_reversed_(), LayoutType::Frame(layout, _, _) => layout.is_reversed_(), LayoutType::Button(layout, _, _) => layout.is_reversed_(), - LayoutType::Visitor(layout) => layout.is_reversed(), } } @@ -288,12 +277,13 @@ impl<'a> Layout<'a> { fn find_id_(&mut self, coord: Coord) -> Option { match &mut self.layout { LayoutType::None => None, + LayoutType::Component(component) => component.find_id(coord), + LayoutType::BoxComponent(layout) => layout.find_id(coord), LayoutType::Single(child) | LayoutType::AlignSingle(child, _) => child.find_id(coord), LayoutType::AlignLayout(layout, _) => layout.find_id_(coord), LayoutType::Frame(child, _, _) => child.find_id_(coord), // Buttons steal clicks, hence Button never returns ID of content LayoutType::Button(_, _, _) => None, - LayoutType::Visitor(layout) => layout.find_id(coord), } } @@ -305,6 +295,8 @@ impl<'a> Layout<'a> { fn draw_(&mut self, mut draw: DrawMgr, id: &WidgetId) { match &mut self.layout { LayoutType::None => (), + LayoutType::Component(component) => component.draw(draw, id), + LayoutType::BoxComponent(layout) => layout.draw(draw, id), LayoutType::Single(child) | LayoutType::AlignSingle(child, _) => child.draw(draw.re()), LayoutType::AlignLayout(layout, _) => layout.draw_(draw, id), LayoutType::Frame(child, storage, style) => { @@ -319,7 +311,6 @@ impl<'a> Layout<'a> { draw.frame(IdRect(id, storage.rect), FrameStyle::Button, bg); child.draw_(draw, id); } - LayoutType::Visitor(layout) => layout.draw(draw, id), } } } @@ -331,7 +322,7 @@ struct List<'a, S, D, I> { children: I, } -impl<'a, S: RowStorage, D: Directional, I> Visitor for List<'a, S, D, I> +impl<'a, S: RowStorage, D: Directional, I> Component for List<'a, S, D, I> where I: ExactSizeIterator>, { @@ -353,7 +344,7 @@ where } } - fn is_reversed(&mut self) -> bool { + fn is_reversed(&self) -> bool { self.direction.is_reversed() } @@ -376,7 +367,7 @@ struct Slice<'a, W: WidgetConfig, D: Directional> { children: &'a mut [W], } -impl<'a, W: WidgetConfig, D: Directional> Visitor for Slice<'a, W, D> { +impl<'a, W: WidgetConfig, D: Directional> Component for Slice<'a, W, D> { fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { let dim = (self.direction, self.children.len()); let mut solver = RowSolver::new(axis, dim, self.data); @@ -395,7 +386,7 @@ impl<'a, W: WidgetConfig, D: Directional> Visitor for Slice<'a, W, D> { } } - fn is_reversed(&mut self) -> bool { + fn is_reversed(&self) -> bool { self.direction.is_reversed() } @@ -419,7 +410,7 @@ struct Grid<'a, S, I> { children: I, } -impl<'a, S: GridStorage, I> Visitor for Grid<'a, S, I> +impl<'a, S: GridStorage, I> Component for Grid<'a, S, I> where I: Iterator)>, { @@ -438,7 +429,7 @@ where } } - fn is_reversed(&mut self) -> bool { + fn is_reversed(&self) -> bool { // TODO: replace is_reversed with direct implementation of spatial_nav false } @@ -472,46 +463,3 @@ impl Storage for FrameStorage { self } } - -/// Layout storage for text element -#[derive(Clone, Default, Debug)] -pub struct TextStorage { - /// Position of text - pub pos: Coord, -} - -struct Text<'a> { - data: &'a mut TextStorage, - text: &'a mut dyn TextApi, - class: TextClass, -} - -impl<'a> Visitor for Text<'a> { - fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { - mgr.text_bound(self.text, self.class, axis) - } - - fn set_rect(&mut self, _mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { - let halign = match self.class { - TextClass::Button => Align::Center, - _ => Align::Default, - }; - self.data.pos = rect.pos; - self.text.update_env(|env| { - env.set_bounds(rect.size.cast()); - env.set_align(align.unwrap_or(halign, Align::Center)); - }); - } - - fn is_reversed(&mut self) -> bool { - false - } - - fn find_id(&mut self, _: Coord) -> Option { - None - } - - fn draw(&mut self, mut draw: DrawMgr, id: &WidgetId) { - draw.text_effects(IdCoord(id, self.data.pos), self.text, self.class); - } -} diff --git a/crates/kas-core/src/lib.rs b/crates/kas-core/src/lib.rs index 8891dc55d..ef228b68b 100644 --- a/crates/kas-core/src/lib.rs +++ b/crates/kas-core/src/lib.rs @@ -24,6 +24,7 @@ mod toolkit; // public implementations: pub mod class; +pub mod component; #[cfg(feature = "config")] #[cfg_attr(doc_cfg, doc(cfg(feature = "config")))] pub mod config; diff --git a/crates/kas-core/src/text.rs b/crates/kas-core/src/text.rs index 8f411a758..cd74d7b3a 100644 --- a/crates/kas-core/src/text.rs +++ b/crates/kas-core/src/text.rs @@ -34,6 +34,8 @@ pub mod util { /// this parameter is a little bit wrong then resizes may sometimes happen /// unnecessarily or may not happen when text is slightly too big (e.g. /// spills into the margin area); this behaviour is probably acceptable. + /// Passing `Size::ZERO` will always resize (unless text is empty). + /// Passing `Size::MAX` should never resize. pub fn set_text_and_prepare( text: &mut Text, s: T, @@ -67,7 +69,10 @@ pub mod util { /// /// Note: an alternative approach would be to delay text preparation by /// adding TkAction::PREPARE and a new method, perhaps in Layout. - fn prepare_if_needed(text: &mut Text, avail: Size) -> TkAction { + pub(crate) fn prepare_if_needed( + text: &mut Text, + avail: Size, + ) -> TkAction { if fonts::fonts().num_faces() == 0 { // Fonts not loaded yet: cannot prepare and can assume it will happen later anyway. return TkAction::empty(); diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 2066ec8f7..851c8c5b2 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -8,7 +8,7 @@ use std::convert::AsRef; use std::ops::{Bound, Range, RangeBounds}; -use super::{FrameStyle, IdCoord, IdRect, SizeHandle, SizeMgr, TextClass}; +use super::{FrameStyle, IdCoord, IdRect, MarkStyle, SizeHandle, SizeMgr, TextClass}; use crate::dir::Direction; use crate::draw::{color::Rgb, Draw, DrawShared, ImageId, PassType}; use crate::event::EventState; @@ -254,6 +254,12 @@ impl<'a> DrawMgr<'a> { self.h.radiobox(f.0, f.1, checked); } + /// Draw UI element: mark + pub fn mark<'b>(&mut self, feature: impl Into>, style: MarkStyle) { + let f = feature.into(); + self.h.mark(f.0, f.1, style); + } + /// Draw UI element: scrollbar /// /// - `id2`: [`WidgetId`] of the handle @@ -417,6 +423,9 @@ pub trait DrawHandle { /// This is similar in appearance to a checkbox. fn radiobox(&mut self, id: &WidgetId, rect: Rect, checked: bool); + /// Draw UI element: mark + fn mark(&mut self, id: &WidgetId, rect: Rect, style: MarkStyle); + /// Draw UI element: scrollbar /// /// - `id`: [`WidgetId`] of the bar diff --git a/crates/kas-core/src/theme/size.rs b/crates/kas-core/src/theme/size.rs index 49a8b1959..5d9b657cd 100644 --- a/crates/kas-core/src/theme/size.rs +++ b/crates/kas-core/src/theme/size.rs @@ -7,13 +7,14 @@ use std::ops::Deref; -#[allow(unused)] -use super::DrawMgr; -use super::{FrameStyle, TextClass}; -use crate::geom::Size; +use super::{FrameStyle, MarkStyle, TextClass}; +use crate::dir::Directional; +use crate::geom::{Size, Vec2}; use crate::layout::{AxisInfo, FrameRules, Margins, SizeRules}; use crate::macros::autoimpl; -use crate::text::TextApi; +use crate::text::{Align, TextApi}; +#[allow(unused)] +use crate::{layout::SetRectMgr, theme::DrawMgr}; // for doc use #[allow(unused)] @@ -88,8 +89,8 @@ impl<'a> SizeMgr<'a> { } /// Size of a frame around another element - pub fn frame(&self, style: FrameStyle, is_vert: bool) -> FrameRules { - self.0.frame(style, is_vert) + pub fn frame(&self, style: FrameStyle, dir: impl Directional) -> FrameRules { + self.0.frame(style, dir.is_vertical()) } /// Size of a separator frame between items @@ -125,19 +126,15 @@ impl<'a> SizeMgr<'a> { self.0.line_height(class) } - /// Update a [`crate::text::Text`] and get a size bound + /// Update a text object, setting font properties and getting a size bound /// - /// First, this method updates the text's [`Environment`]: `bounds`, `dpp` - /// and `pt_size` are set. Second, the text is prepared (which is necessary - /// to calculate size requirements). Finally, this converts the requirements - /// to a [`SizeRules`] value and returns it. + /// This method updates the text's [`Environment`] and uses the result to + /// calculate size requirements. /// - /// Usually this method is used in [`Layout::size_rules`], then - /// [`TextApiExt::update_env`] is used in [`Layout::set_rect`]. + /// It is necessary to update the environment *again* once the target `rect` + /// is known: use [`SetRectMgr::text_set_size`] to do this. /// /// [`Environment`]: crate::text::Environment - /// [`Layout::set_rect`]: crate::Layout::set_rect - /// [`Layout::size_rules`]: crate::Layout::size_rules pub fn text_bound( &self, text: &mut dyn TextApi, @@ -147,11 +144,6 @@ impl<'a> SizeMgr<'a> { self.0.text_bound(text, class, axis) } - /// Width of an edit marker - pub fn text_cursor_width(&self) -> f32 { - self.0.text_cursor_width() - } - /// Size of the element drawn by [`DrawMgr::checkbox`]. pub fn checkbox(&self) -> Size { self.0.checkbox() @@ -162,6 +154,11 @@ impl<'a> SizeMgr<'a> { self.0.radiobox() } + /// A simple mark + pub fn mark(&self, style: MarkStyle, dir: impl Directional) -> SizeRules { + self.0.mark(style, dir.is_vertical()) + } + /// Dimensions for a scrollbar /// /// Returns: @@ -240,23 +237,27 @@ pub trait SizeHandle { /// The height of a line of text fn line_height(&self, class: TextClass) -> i32; - /// Update a [`crate::text::Text`] and get a size bound + /// Update a text object, setting font properties and getting a size bound /// - /// First, this method updates the text's [`Environment`]: `bounds`, `dpp` - /// and `pt_size` are set. Second, the text is prepared (which is necessary - /// to calculate size requirements). Finally, this converts the requirements - /// to a [`SizeRules`] value and returns it. + /// This method updates the text's [`Environment`] and uses the result to + /// calculate size requirements. /// - /// Usually this method is used in [`Layout::size_rules`], then - /// [`TextApiExt::update_env`] is used in [`Layout::set_rect`]. + /// It is necessary to update the environment *again* once the target `rect` + /// is known: use [`Self::text_set_size`] to do this. /// /// [`Environment`]: crate::text::Environment - /// [`Layout::set_rect`]: crate::Layout::set_rect - /// [`Layout::size_rules`]: crate::Layout::size_rules fn text_bound(&self, text: &mut dyn TextApi, class: TextClass, axis: AxisInfo) -> SizeRules; - /// Width of an edit marker - fn text_cursor_width(&self) -> f32; + /// Update a text object, setting font properties and wrap size + /// + /// Returns required size. + fn text_set_size( + &self, + text: &mut dyn TextApi, + class: TextClass, + size: Size, + align: (Align, Align), + ) -> Vec2; /// Size of the element drawn by [`DrawMgr::checkbox`]. fn checkbox(&self) -> Size; @@ -264,6 +265,9 @@ pub trait SizeHandle { /// Size of the element drawn by [`DrawMgr::radiobox`]. fn radiobox(&self) -> Size; + /// A simple mark + fn mark(&self, style: MarkStyle, is_vert: bool) -> SizeRules; + /// Dimensions for a scrollbar /// /// Returns: diff --git a/crates/kas-core/src/theme/style.rs b/crates/kas-core/src/theme/style.rs index 4acf5b4c7..366678a95 100644 --- a/crates/kas-core/src/theme/style.rs +++ b/crates/kas-core/src/theme/style.rs @@ -5,6 +5,8 @@ //! Theme style components +use crate::dir::Direction; + /// Class of text drawn /// /// Themes choose font, font size, colour, and alignment based on this. @@ -88,3 +90,10 @@ pub enum FrameStyle { /// Box used to contain editable text EditBox, } + +/// Style of marks +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum MarkStyle { + /// An arrowhead/angle-bracket/triangle pointing in the given direction + Point(Direction), +} diff --git a/crates/kas-theme/src/dim.rs b/crates/kas-theme/src/dim.rs index 0c97a94ce..3f9bdd09e 100644 --- a/crates/kas-theme/src/dim.rs +++ b/crates/kas-theme/src/dim.rs @@ -12,10 +12,11 @@ use std::rc::Rc; use crate::anim::AnimState; use kas::cast::traits::*; +use kas::dir::Directional; use kas::geom::{Size, Vec2}; use kas::layout::{AxisInfo, FrameRules, Margins, SizeRules, Stretch}; use kas::text::{fonts::FontId, Align, TextApi, TextApiExt}; -use kas::theme::{FrameStyle, SizeHandle, TextClass}; +use kas::theme::{FrameStyle, MarkStyle, SizeHandle, TextClass}; /// Parameterisation of [`Dimensions`] /// @@ -39,6 +40,8 @@ pub struct Parameters { pub button_frame: f32, /// CheckBox inner size in Points pub checkbox_inner: f32, + /// Larger size of a mark in Points + pub mark: f32, /// Scrollbar minimum handle size pub scrollbar_size: Vec2, /// Slider minimum handle size @@ -57,7 +60,7 @@ pub struct Dimensions { pub scale_factor: f32, pub dpp: f32, pub pt_size: f32, - pub font_marker_width: f32, + pub mark_line: f32, pub line_height: i32, pub min_line_length: i32, pub outer_margin: u16, @@ -68,6 +71,7 @@ pub struct Dimensions { pub menu_frame: i32, pub button_frame: i32, pub checkbox: i32, + pub mark: i32, pub scrollbar: Size, pub slider: Size, pub progress_bar: Size, @@ -101,7 +105,7 @@ impl Dimensions { scale_factor, dpp, pt_size, - font_marker_width: (1.6 * scale_factor).round().max(1.0), + mark_line: (1.2 * dpp).round().max(1.0), line_height, min_line_length: (8.0 * dpem).cast_nearest(), outer_margin, @@ -113,6 +117,7 @@ impl Dimensions { button_frame: (params.button_frame * scale_factor).cast_nearest(), checkbox: i32::conv_nearest(params.checkbox_inner * dpp) + 2 * (i32::from(inner_margin) + frame), + mark: i32::conv_nearest(params.mark * dpp), scrollbar: Size::conv_nearest(params.scrollbar_size * scale_factor), slider: Size::conv_nearest(params.slider_size * scale_factor), progress_bar: Size::conv_nearest(params.progress_bar * scale_factor), @@ -208,41 +213,49 @@ impl SizeHandle for Window { } fn text_bound(&self, text: &mut dyn TextApi, class: TextClass, axis: AxisInfo) -> SizeRules { - // Note: for horizontal axis of Edit* classes, input text does not affect size rules. - // We must set env at least once, but do for vertical axis anyway. - let mut required = None; - if !axis.is_horizontal() || !matches!(class, TextClass::Edit(_)) { - required = Some(text.update_env(|env| { - if let Some(font_id) = self.fonts.get(&class).cloned() { - env.set_font_id(font_id); - } - env.set_dpp(self.dims.dpp); - env.set_pt_size(self.dims.pt_size); - - let mut bounds = kas::text::Vec2::INFINITY; - if let Some(size) = axis.size_other_if_fixed(false) { - bounds.1 = size.cast(); - } else if let Some(size) = axis.size_other_if_fixed(true) { - bounds.0 = size.cast(); - } - env.set_bounds(bounds); - env.set_align((Align::TL, Align::TL)); // force top-left alignment for sizing - env.set_wrap(class.multi_line()); - })); - } - let margin = match axis.is_horizontal() { true => self.dims.text_margin.0, false => self.dims.text_margin.1, }; let margins = (margin, margin); + + // Note: for horizontal axis of Edit* classes, input text does not affect size rules. + if axis.is_horizontal() { + if let TextClass::Edit(multi) = class { + let min = self.dims.min_line_length; + let (min, ideal) = match multi { + false => (min, 2 * min), + true => (min, 3 * min), + }; + return SizeRules::new(min, ideal, margins, Stretch::Low); + } + } + + let required = text.update_env(|env| { + if let Some(font_id) = self.fonts.get(&class).cloned() { + env.set_font_id(font_id); + } + env.set_dpp(self.dims.dpp); + env.set_pt_size(self.dims.pt_size); + + let mut bounds = kas::text::Vec2::INFINITY; + if let Some(size) = axis.size_other_if_fixed(false) { + bounds.1 = size.cast(); + } else if let Some(size) = axis.size_other_if_fixed(true) { + bounds.0 = size.cast(); + } + env.set_bounds(bounds); + env.set_align((Align::TL, Align::TL)); // force top-left alignment for sizing + env.set_wrap(class.multi_line()); + }); + if axis.is_horizontal() { let min = self.dims.min_line_length; let (min, ideal) = match class { TextClass::Edit(false) => (min, 2 * min), TextClass::Edit(true) => (min, 3 * min), _ => { - let bound = i32::conv_ceil(required.unwrap().0); + let bound = i32::conv_ceil(required.0); (bound.min(min), bound.min(3 * min)) } }; @@ -256,7 +269,7 @@ impl SizeHandle for Window { }; SizeRules::new(min, ideal, margins, stretch) } else { - let bound = i32::conv_ceil(required.unwrap().1); + let bound = i32::conv_ceil(required.1); let min = match class { _ if class.single_line() => self.dims.line_height, TextClass::Label(true) => bound, @@ -273,8 +286,26 @@ impl SizeHandle for Window { } } - fn text_cursor_width(&self) -> f32 { - self.dims.font_marker_width + fn text_set_size( + &self, + text: &mut dyn TextApi, + class: TextClass, + size: Size, + align: (Align, Align), + ) -> Vec2 { + // TODO(opt): we don't always need to do this work + text.update_env(|env| { + if let Some(font_id) = self.fonts.get(&class).cloned() { + env.set_font_id(font_id); + } + env.set_dpp(self.dims.dpp); + env.set_pt_size(self.dims.pt_size); + + env.set_bounds(size.cast()); + env.set_align(align); + env.set_wrap(class.multi_line()); + }) + .into() } fn checkbox(&self) -> Size { @@ -286,6 +317,19 @@ impl SizeHandle for Window { self.checkbox() } + fn mark(&self, style: MarkStyle, is_vert: bool) -> SizeRules { + match style { + MarkStyle::Point(dir) => { + let w = match dir.is_vertical() == is_vert { + true => self.dims.mark / 2 + i32::conv_ceil(self.dims.mark_line), + false => self.dims.mark + i32::conv_ceil(self.dims.mark_line), + }; + let m = self.dims.outer_margin; + SizeRules::fixed(w, (m, m)) + } + } + } + fn scrollbar(&self) -> (Size, i32) { let size = self.dims.scrollbar; (size, 3 * size.0) diff --git a/crates/kas-theme/src/flat_theme.rs b/crates/kas-theme/src/flat_theme.rs index 84fc73524..0fcdb79e6 100644 --- a/crates/kas-theme/src/flat_theme.rs +++ b/crates/kas-theme/src/flat_theme.rs @@ -20,7 +20,7 @@ use kas::event::EventState; use kas::geom::*; use kas::text::{fonts, Effect, TextApi, TextDisplay}; use kas::theme::{self, SizeHandle, ThemeControl}; -use kas::theme::{Background, FrameStyle, TextClass}; +use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; use kas::{TkAction, WidgetId}; // Used to ensure a rectangular background is inside a circular corner. @@ -114,6 +114,7 @@ const DIMS: dim::Parameters = dim::Parameters { // NOTE: visual thickness is (button_frame * scale_factor).round() * (1 - BG_SHRINK_FACTOR) button_frame: 2.4, checkbox_inner: 7.0, + mark: 8.0, scrollbar_size: Vec2::splat(8.0), slider_size: Vec2(16.0, 16.0), progress_bar: Vec2::splat(8.0), @@ -516,7 +517,7 @@ where return; } - let width = self.w.dims.font_marker_width; + let width = self.w.dims.mark_line; let pos = Vec2::conv(pos); let mut col = self.cols.nav_focus; @@ -614,6 +615,48 @@ where } } + fn mark(&mut self, id: &WidgetId, rect: Rect, style: MarkStyle) { + let col = if self.ev.is_disabled(id) { + self.cols.text_disabled + } else { + self.cols.text + }; + + match style { + MarkStyle::Point(dir) => { + let size = match dir.is_horizontal() { + true => Size(self.w.dims.mark / 2, self.w.dims.mark), + false => Size(self.w.dims.mark, self.w.dims.mark / 2), + }; + let offset = Offset::conv(rect.size.clamped_sub(size) / 2); + let q = Quad::conv(Rect::new(rect.pos + offset, size)); + + let (p1, p2, p3); + if dir.is_horizontal() { + let (mut x1, mut x2) = (q.a.0, q.b.0); + if dir.is_reversed() { + std::mem::swap(&mut x1, &mut x2); + } + p1 = Vec2(x1, q.a.1); + p2 = Vec2(x2, 0.5 * (q.a.1 + q.b.1)); + p3 = Vec2(x1, q.b.1); + } else { + let (mut y1, mut y2) = (q.a.1, q.b.1); + if dir.is_reversed() { + std::mem::swap(&mut y1, &mut y2); + } + p1 = Vec2(q.a.0, y1); + p2 = Vec2(0.5 * (q.a.0 + q.b.0), y2); + p3 = Vec2(q.b.0, y1); + }; + + let f = 0.5 * self.w.dims.mark_line; + self.draw.rounded_line(p1, p2, f, col); + self.draw.rounded_line(p2, p3, f, col); + } + } + } + fn scrollbar(&mut self, id: &WidgetId, id2: &WidgetId, rect: Rect, h_rect: Rect, _: Direction) { // track let outer = Quad::conv(rect); diff --git a/crates/kas-theme/src/shaded_theme.rs b/crates/kas-theme/src/shaded_theme.rs index 50b3063ea..27fef2205 100644 --- a/crates/kas-theme/src/shaded_theme.rs +++ b/crates/kas-theme/src/shaded_theme.rs @@ -17,7 +17,7 @@ use kas::event::EventState; use kas::geom::*; use kas::text::{TextApi, TextDisplay}; use kas::theme::{self, Background, SizeHandle, ThemeControl}; -use kas::theme::{FrameStyle, TextClass}; +use kas::theme::{FrameStyle, MarkStyle, TextClass}; use kas::{TkAction, WidgetId}; /// A theme using simple shading to give apparent depth to elements @@ -70,6 +70,7 @@ const DIMS: dim::Parameters = dim::Parameters { menu_frame: 2.4, button_frame: 5.0, checkbox_inner: 9.0, + mark: 9.0, scrollbar_size: Vec2::splat(8.0), slider_size: Vec2(12.0, 25.0), progress_bar: Vec2::splat(12.0), @@ -400,6 +401,10 @@ where } } + fn mark(&mut self, id: &WidgetId, rect: Rect, style: MarkStyle) { + self.as_flat().mark(id, rect, style); + } + fn scrollbar(&mut self, id: &WidgetId, id2: &WidgetId, rect: Rect, h_rect: Rect, _: Direction) { // track let outer = Quad::conv(rect); diff --git a/crates/kas-widgets/src/adapter/label.rs b/crates/kas-widgets/src/adapter/label.rs index fdf56ed1c..28e4482e1 100644 --- a/crates/kas-widgets/src/adapter/label.rs +++ b/crates/kas-widgets/src/adapter/label.rs @@ -5,7 +5,7 @@ //! Wrapper adding a label -use kas::text::util::set_text_and_prepare; +use kas::component::Label; use kas::theme::TextClass; use kas::{event, layout, prelude::*}; @@ -27,8 +27,7 @@ impl_scope! { inner: W, wrap: bool, layout_store: layout::FixedRowStorage<2>, - label_store: layout::TextStorage, - label: Text, + label: Label, } impl Self where D: Default { @@ -43,14 +42,14 @@ impl_scope! { /// Construct from `direction`, `inner` widget and `label` #[inline] pub fn new_with_direction>(direction: D, inner: W, label: T) -> Self { + let wrap = true; WithLabel { core: Default::default(), dir: direction, inner, - wrap: true, + wrap, layout_store: Default::default(), - label_store: Default::default(), - label: Text::new_multi(label.into()), + label: Label::new(label.into(), TextClass::Label(wrap)), } } @@ -60,10 +59,19 @@ impl_scope! { self.dir.as_direction() } - /// Deconstruct into `(inner, label)` + /// Take inner #[inline] - pub fn deconstruct(self) -> (W, Text) { - (self.inner, self.label) + pub fn take_inner(self) -> W { + self.inner + } + + /// Access layout storage + /// + /// The number of columns/rows is fixed at two: the `inner` widget, and + /// the `label` (in this order, regardless of direction). + #[inline] + pub fn layout_storage(&mut self) -> &mut impl layout::RowStorage { + &mut self.layout_store } /// Get whether line-wrapping is enabled @@ -92,12 +100,12 @@ impl_scope! { /// Note: this must not be called before fonts have been initialised /// (usually done by the theme when the main loop starts). pub fn set_text>(&mut self, text: T) -> TkAction { - set_text_and_prepare(&mut self.label, text.into(), self.core.rect.size) + self.label.set_text_and_prepare(text.into(), self.core.rect.size) } /// Get the accelerator keys pub fn keys(&self) -> &[event::VirtualKeyCode] { - self.label.text().keys() + self.label.keys() } } @@ -111,7 +119,7 @@ impl_scope! { fn layout(&mut self) -> layout::Layout<'_> { let arr = [ layout::Layout::single(&mut self.inner), - layout::Layout::text(&mut self.label_store, &mut self.label, TextClass::Label(self.wrap)), + layout::Layout::component(&mut self.label), ]; layout::Layout::list(arr.into_iter(), self.dir, &mut self.layout_store) } @@ -133,10 +141,10 @@ impl_scope! { impl SetAccel for Self { fn set_accel_string(&mut self, string: AccelString) -> TkAction { let mut action = TkAction::empty(); - if self.label.text().keys() != string.keys() { + if self.label.keys() != string.keys() { action |= TkAction::RECONFIGURE; } - action | set_text_and_prepare(&mut self.label, string, self.core.rect.size) + action | self.label.set_text_and_prepare(string, self.core.rect.size) } } } diff --git a/crates/kas-widgets/src/adapter/map.rs b/crates/kas-widgets/src/adapter/map.rs index 53d253108..604f09f80 100644 --- a/crates/kas-widgets/src/adapter/map.rs +++ b/crates/kas-widgets/src/adapter/map.rs @@ -5,7 +5,7 @@ //! Message Map widget -use crate::Menu; +use crate::menu; use kas::prelude::*; use std::rc::Rc; @@ -68,7 +68,10 @@ impl_scope! { } } - impl Menu for MapResponse { + impl menu::Menu for MapResponse { + fn sub_items(&mut self) -> Option { + self.inner.sub_items() + } fn menu_is_open(&self) -> bool { self.inner.menu_is_open() } diff --git a/crates/kas-widgets/src/button.rs b/crates/kas-widgets/src/button.rs index 902ababa2..692057d1f 100644 --- a/crates/kas-widgets/src/button.rs +++ b/crates/kas-widgets/src/button.rs @@ -5,6 +5,7 @@ //! Push-buttons +use kas::component::Label; use kas::draw::color::Rgb; use kas::event::{self, VirtualKeyCode, VirtualKeyCodes}; use kas::layout; @@ -180,17 +181,16 @@ impl_scope! { #[widget_core] core: kas::CoreData, keys1: VirtualKeyCodes, + label: Label, layout_frame: layout::FrameStorage, - layout_text: layout::TextStorage, color: Option, - label: Text, on_push: Option Option>>, } impl WidgetConfig for Self { fn configure(&mut self, mgr: &mut SetRectMgr) { mgr.add_accel_keys(self.id_ref(), &self.keys1); - mgr.add_accel_keys(self.id_ref(), self.label.text().keys()); + mgr.add_accel_keys(self.id_ref(), self.label.keys()); } fn key_nav(&self) -> bool { @@ -203,7 +203,7 @@ impl_scope! { impl Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { - let inner = layout::Layout::text(&mut self.layout_text, &mut self.label, TextClass::Button); + let inner = layout::Layout::component(&mut self.label); layout::Layout::button(&mut self.layout_frame, inner, self.color) } } @@ -212,15 +212,12 @@ impl_scope! { /// Construct a button with given `label` #[inline] pub fn new>(label: S) -> Self { - let label = label.into(); - let text = Text::new_single(label); TextButton { core: Default::default(), keys1: Default::default(), + label: Label::new(label.into(), TextClass::Button), layout_frame: Default::default(), - layout_text: Default::default(), color: None, - label: text, on_push: None, } } @@ -240,7 +237,6 @@ impl_scope! { core: self.core, keys1: self.keys1, layout_frame: self.layout_frame, - layout_text: self.layout_text, color: self.color, label: self.label, on_push: Some(Rc::new(f)), @@ -307,11 +303,11 @@ impl_scope! { impl SetAccel for Self { fn set_accel_string(&mut self, string: AccelString) -> TkAction { let mut action = TkAction::empty(); - if self.label.text().keys() != string.keys() { + if self.label.keys() != string.keys() { action |= TkAction::RECONFIGURE; } let avail = self.core.rect.size.clamped_sub(self.layout_frame.size); - action | kas::text::util::set_text_and_prepare(&mut self.label, string, avail) + action | self.label.set_text_and_prepare(string, avail) } } diff --git a/crates/kas-widgets/src/combobox.rs b/crates/kas-widgets/src/combobox.rs index 6837c61f4..2e1f26f49 100644 --- a/crates/kas-widgets/src/combobox.rs +++ b/crates/kas-widgets/src/combobox.rs @@ -5,11 +5,12 @@ //! Combobox -use super::{IndexedColumn, MenuEntry, PopupFrame}; +use super::{menu::MenuEntry, IndexedColumn, PopupFrame}; +use kas::component::{Label, Mark}; use kas::event::{self, Command}; use kas::layout; use kas::prelude::*; -use kas::theme::TextClass; +use kas::theme::{MarkStyle, TextClass}; use kas::WindowId; use std::rc::Rc; @@ -26,9 +27,10 @@ impl_scope! { pub struct ComboBox { #[widget_core] core: CoreData, - label: Text, + label: Label, + mark: Mark, + layout_list: layout::FixedRowStorage<2>, layout_frame: layout::FrameStorage, - layout_text: layout::TextStorage, #[widget] popup: ComboPopup, active: usize, @@ -39,8 +41,12 @@ impl_scope! { impl kas::Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { - let inner = layout::Layout::text(&mut self.layout_text, &mut self.label, TextClass::Button); - layout::Layout::button(&mut self.layout_frame, inner, None) + let list = [ + layout::Layout::component(&mut self.label), + layout::Layout::component(&mut self.mark), + ]; + let list = layout::Layout::list(list.into_iter(), Direction::Right, &mut self.layout_list); + layout::Layout::button(&mut self.layout_frame, list, None) } fn spatial_nav(&mut self, _: &mut SetRectMgr, _: bool, _: Option) -> Option { @@ -211,12 +217,13 @@ impl ComboBox { #[inline] pub fn new(entries: Vec>, active: usize) -> Self { let label = entries.get(active).map(|entry| entry.get_string()); - let label = Text::new_single(label.unwrap_or("".to_string())); + let label = Label::new(label.unwrap_or("".to_string()), TextClass::Button); ComboBox { core: Default::default(), label, + mark: Mark::new(MarkStyle::Point(Direction::Down)), + layout_list: Default::default(), layout_frame: Default::default(), - layout_text: Default::default(), popup: ComboPopup { core: Default::default(), inner: PopupFrame::new(IndexedColumn::new(entries)), @@ -242,8 +249,9 @@ impl ComboBox { ComboBox { core: self.core, label: self.label, + mark: self.mark, + layout_list: self.layout_list, layout_frame: self.layout_frame, - layout_text: self.layout_text, popup: self.popup, active: self.active, opening: self.opening, @@ -274,7 +282,7 @@ impl ComboBox { "".to_string() }; let avail = self.core.rect.size.clamped_sub(self.layout_frame.size); - kas::text::util::set_text_and_prepare(&mut self.label, string, avail) + self.label.set_text_and_prepare(string, avail) } else { TkAction::empty() } @@ -332,7 +340,9 @@ impl ComboBox { /// /// Panics if `index` is out of bounds. pub fn replace>(&mut self, mgr: &mut SetRectMgr, index: usize, label: T) { - *mgr |= self.popup.inner[index].set_accel(label); + self.popup + .inner + .replace(mgr, index, MenuEntry::new(label, ())); } } diff --git a/crates/kas-widgets/src/edit_field.rs b/crates/kas-widgets/src/edit_field.rs index 9963a6bb4..bcd1ffdf2 100644 --- a/crates/kas-widgets/src/edit_field.rs +++ b/crates/kas-widgets/src/edit_field.rs @@ -383,22 +383,17 @@ impl_scope! { size_mgr.text_bound(&mut self.text, class, axis) } - fn set_rect(&mut self, _: &mut SetRectMgr, rect: Rect, align: AlignHints) { + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { let valign = if self.multi_line { Align::Default } else { Align::Center }; + let class = TextClass::Edit(self.multi_line); self.core.rect = rect; - let size = rect.size; - self.required = self - .text - .update_env(|env| { - env.set_align(align.unwrap_or(Align::Default, valign)); - env.set_bounds(size.cast()); - }) - .into(); + let align = align.unwrap_or(Align::Default, valign); + self.required = mgr.text_set_size(&mut self.text, class, rect.size, align); self.set_view_offset_from_edit_pos(); } diff --git a/crates/kas-widgets/src/grid.rs b/crates/kas-widgets/src/grid.rs index d5da69f98..7bafb0529 100644 --- a/crates/kas-widgets/src/grid.rs +++ b/crates/kas-widgets/src/grid.rs @@ -116,6 +116,22 @@ impl Grid { grid } + /// Get grid dimensions + /// + /// The numbers of rows, columns and spans is determined automatically. + #[inline] + pub fn dimensions(&self) -> GridDimensions { + self.dim + } + + /// Access layout storage + /// + /// Use [`Self::dimensions`] to get expected dimensions. + #[inline] + pub fn layout_storage(&mut self) -> &mut impl layout::GridStorage { + &mut self.data + } + fn calc_dim(&mut self) { let mut dim = GridDimensions::default(); for child in &self.widgets { diff --git a/crates/kas-widgets/src/label.rs b/crates/kas-widgets/src/label.rs index 2554e00ed..0f90d8b75 100644 --- a/crates/kas-widgets/src/label.rs +++ b/crates/kas-widgets/src/label.rs @@ -70,12 +70,10 @@ impl_scope! { size_mgr.text_bound(&mut self.label, TextClass::Label(self.wrap), axis) } - fn set_rect(&mut self, _: &mut SetRectMgr, rect: Rect, align: AlignHints) { + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { self.core.rect = rect; - self.label.update_env(|env| { - env.set_bounds(rect.size.cast()); - env.set_align(align.unwrap_or(Align::Default, Align::Center)); - }); + let align = align.unwrap_or(Align::Default, Align::Center); + mgr.text_set_size(&mut self.label, TextClass::Label(self.wrap), rect.size, align); } #[cfg(feature = "min_spec")] diff --git a/crates/kas-widgets/src/lib.rs b/crates/kas-widgets/src/lib.rs index 4bec8f882..d47b15cd7 100644 --- a/crates/kas-widgets/src/lib.rs +++ b/crates/kas-widgets/src/lib.rs @@ -21,9 +21,7 @@ //! //! ## Menus //! -//! - [`ComboBox`]: a simple pop-up selector -//! - [`MenuBar`], [`SubMenu`]: menu parent widgets -//! - [`MenuEntry`], [`MenuToggle`], [`Separator`]: menu entries +//! See the [`menu`] module. //! //! ## Controls //! @@ -73,7 +71,7 @@ mod label; mod list; #[macro_use] mod macros; -mod menu; +pub mod menu; mod nav_frame; mod progress; mod radiobox; @@ -101,7 +99,6 @@ pub use frame::{Frame, PopupFrame}; pub use grid::{BoxGrid, Grid}; pub use label::{AccelLabel, Label, StrLabel, StringLabel}; pub use list::*; -pub use menu::*; pub use nav_frame::NavFrame; pub use progress::ProgressBar; pub use radiobox::{RadioBox, RadioBoxBare, RadioBoxGroup}; diff --git a/crates/kas-widgets/src/list.rs b/crates/kas-widgets/src/list.rs index 1828d13f7..46c8a993e 100644 --- a/crates/kas-widgets/src/list.rs +++ b/crates/kas-widgets/src/list.rs @@ -219,20 +219,29 @@ impl_scope! { impl Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { - if self.size_solved { - layout::Layout::slice(&mut self.widgets, self.direction, &mut self.layout_store) - } else { - // Draw without sizing all elements may cause a panic, so don't. - Default::default() - } + layout::Layout::slice(&mut self.widgets, self.direction, &mut self.layout_store) } - fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules { - // Assumption: if size_rules is called, then set_rect will be too. + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { + self.core_data_mut().rect = rect; + self.layout().set_rect(mgr, rect, align); self.size_solved = true; + } - layout::Layout::slice(&mut self.widgets, self.direction, &mut self.layout_store) - .size_rules(size_mgr, axis) + fn find_id(&mut self, coord: Coord) -> Option { + if !self.rect().contains(coord) || !self.size_solved { + return None; + } + + let coord = coord + self.translation(); + self.layout().find_id(coord).or_else(|| Some(self.id())) + } + + fn draw(&mut self, draw: DrawMgr) { + if self.size_solved { + let id = self.id(); // clone to avoid borrow conflict + self.layout().draw(draw, &id); + } } } @@ -336,6 +345,14 @@ impl_scope! { self.direction.as_direction() } + /// Access layout storage + /// + /// The number of columns/rows is [`Self.len`]. + #[inline] + pub fn layout_storage(&mut self) -> &mut impl layout::RowStorage { + &mut self.layout_store + } + /// True if there are no child widgets pub fn is_empty(&self) -> bool { self.widgets.is_empty() diff --git a/crates/kas-widgets/src/menu.rs b/crates/kas-widgets/src/menu.rs index f2e4182a6..a05b5a814 100644 --- a/crates/kas-widgets/src/menu.rs +++ b/crates/kas-widgets/src/menu.rs @@ -3,10 +3,23 @@ // You may obtain a copy of the License in the LICENSE-APACHE file or at: // https://www.apache.org/licenses/LICENSE-2.0 -//! Menus +//! Menu widgets +//! +//! The following serve as menu roots: +//! +//! - [`crate::ComboBox`] +//! - [`MenuBar`] +//! +//! Any implementation of the [`Menu`] trait may be used as a menu item: +//! +//! - [`SubMenu`] +//! - [`MenuEntry`] +//! - [`MenuToggle`] +//! - [`Separator`] use crate::adapter::AdaptWidget; use crate::Separator; +use kas::component::Component; use kas::dir::Right; use kas::prelude::*; use std::fmt::Debug; @@ -19,10 +32,41 @@ pub use menu_entry::{MenuEntry, MenuToggle}; pub use menubar::{MenuBar, MenuBuilder}; pub use submenu::SubMenu; +/// Return value of [`Menu::sub_items`] +#[derive(Default)] +pub struct SubItems<'a> { + /// Primary label + pub label: Option<&'a mut dyn Component>, + /// Secondary label, often used to show shortcut key + pub label2: Option<&'a mut dyn Component>, + /// Sub-menu indicator + pub submenu: Option<&'a mut dyn Component>, + /// Icon + pub icon: Option<&'a mut dyn Component>, + /// Toggle mark + // TODO: should be a component? + pub toggle: Option<&'a mut dyn WidgetConfig>, +} + /// Trait governing menus, sub-menus and menu-entries #[autoimpl(for Box)] pub trait Menu: Widget { - /// Report whether one's own menu is open + /// Access row items for aligned layout + /// + /// If this is implemented, the row will be sized and layout through direct + /// access to these sub-components. [`Layout::size_rules`] will not be + /// invoked on `self`. [`Layout::set_rect`] will be, but should not set the + /// position of these items. [`Layout::draw`] should draw all components, + /// including a frame with style [`kas::theme::FrameStyle::MenuEntry`] on + /// self. + /// + /// Return value is `None` or `Some((label, opt_label2, opt_submenu, opt_icon, opt_toggle))`. + /// `opt_label2` is used to show shortcut labels. `opt_submenu` is a sub-menu indicator. + fn sub_items(&mut self) -> Option { + None + } + + /// Report whether a submenu (if any) is open /// /// By default, this is `false`. fn menu_is_open(&self) -> bool { diff --git a/crates/kas-widgets/src/menu/menu_entry.rs b/crates/kas-widgets/src/menu/menu_entry.rs index 082319b90..9753e4c5d 100644 --- a/crates/kas-widgets/src/menu/menu_entry.rs +++ b/crates/kas-widgets/src/menu/menu_entry.rs @@ -5,9 +5,10 @@ //! Menu Entries -use super::Menu; +use super::{Menu, SubItems}; use crate::CheckBoxBare; -use kas::theme::{FrameStyle, TextClass}; +use kas::component::{Component, Label}; +use kas::theme::{FrameStyle, IdRect, TextClass}; use kas::{layout, prelude::*}; use std::fmt::Debug; @@ -18,15 +19,13 @@ impl_scope! { pub struct MenuEntry { #[widget_core] core: kas::CoreData, - label: Text, - layout_label: layout::TextStorage, - layout_frame: layout::FrameStorage, + label: Label, msg: M, } impl WidgetConfig for Self { fn configure(&mut self, mgr: &mut SetRectMgr) { - mgr.add_accel_keys(self.id_ref(), self.label.text().keys()); + mgr.add_accel_keys(self.id_ref(), self.label.keys()); } fn key_nav(&self) -> bool { @@ -36,17 +35,12 @@ impl_scope! { impl Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { - let inner = layout::Layout::text(&mut self.layout_label, &mut self.label, TextClass::MenuLabel); - layout::Layout::frame(&mut self.layout_frame, inner, FrameStyle::MenuEntry) + layout::Layout::component(&mut self.label) } fn draw(&mut self, mut draw: DrawMgr) { draw.frame(&*self, FrameStyle::MenuEntry, Default::default()); - draw.text_effects( - kas::theme::IdCoord(self.id_ref(), self.layout_label.pos), - &self.label, - TextClass::MenuLabel, - ); + self.label.draw(draw, &self.core.id); } } @@ -59,9 +53,7 @@ impl_scope! { pub fn new>(label: S, msg: M) -> Self { MenuEntry { core: Default::default(), - label: Text::new_single(label.into()), - layout_label: Default::default(), - layout_frame: Default::default(), + label: Label::new(label.into(), TextClass::MenuLabel), msg, } } @@ -81,11 +73,11 @@ impl_scope! { impl SetAccel for Self { fn set_accel_string(&mut self, string: AccelString) -> TkAction { let mut action = TkAction::empty(); - if self.label.text().keys() != string.keys() { + if self.label.keys() != string.keys() { action |= TkAction::RECONFIGURE; } - let avail = self.core.rect.size.clamped_sub(self.layout_frame.size); - action | kas::text::util::set_text_and_prepare(&mut self.label, string, avail) + let avail = self.core.rect.size; + action | self.label.set_text_and_prepare(string, avail) } } @@ -100,7 +92,14 @@ impl_scope! { } } - impl Menu for Self {} + impl Menu for Self { + fn sub_items(&mut self) -> Option { + Some(SubItems { + label: Some(&mut self.label), + ..Default::default() + }) + } + } } impl_scope! { @@ -114,15 +113,13 @@ impl_scope! { core: CoreData, #[widget] checkbox: CheckBoxBare, - label: Text, - layout_label: layout::TextStorage, + label: Label, layout_list: layout::DynRowStorage, - layout_frame: layout::FrameStorage, } impl WidgetConfig for Self { fn configure(&mut self, mgr: &mut SetRectMgr) { - mgr.add_accel_keys(self.checkbox.id_ref(), self.label.text().keys()); + mgr.add_accel_keys(self.checkbox.id_ref(), self.label.keys()); } } @@ -130,10 +127,9 @@ impl_scope! { fn layout(&mut self) -> layout::Layout<'_> { let list = [ layout::Layout::single(&mut self.checkbox), - layout::Layout::text(&mut self.layout_label, &mut self.label, TextClass::MenuLabel), + layout::Layout::component(&mut self.label), ]; - let inner = layout::Layout::list(list.into_iter(), Direction::Right, &mut self.layout_list); - layout::Layout::frame(&mut self.layout_frame, inner, FrameStyle::MenuEntry) + layout::Layout::list(list.into_iter(), Direction::Right, &mut self.layout_list) } fn find_id(&mut self, coord: Coord) -> Option { @@ -144,8 +140,8 @@ impl_scope! { } fn draw(&mut self, mut draw: DrawMgr) { - draw.frame(&*self, FrameStyle::MenuEntry, Default::default()); - let id = self.id(); + let id = self.checkbox.id(); + draw.frame(IdRect(&id, self.rect()), FrameStyle::MenuEntry, Default::default()); self.layout().draw(draw, &id); } } @@ -154,7 +150,15 @@ impl_scope! { type Msg = M; } - impl Menu for Self {} + impl Menu for Self { + fn sub_items(&mut self) -> Option { + Some(SubItems { + label: Some(&mut self.label), + toggle: Some(&mut self.checkbox), + ..Default::default() + }) + } + } impl MenuToggle { /// Construct a toggleable menu entry with a given `label` @@ -163,10 +167,8 @@ impl_scope! { MenuToggle { core: Default::default(), checkbox: CheckBoxBare::new(), - label: Text::new_single(label.into()), - layout_label: Default::default(), + label: Label::new(label.into(), TextClass::MenuLabel), layout_list: Default::default(), - layout_frame: Default::default(), } } @@ -185,9 +187,7 @@ impl_scope! { core: self.core, checkbox: self.checkbox.on_toggle(f), label: self.label, - layout_label: self.layout_label, layout_list: self.layout_list, - layout_frame: self.layout_frame, } } } diff --git a/crates/kas-widgets/src/menu/menubar.rs b/crates/kas-widgets/src/menu/menubar.rs index ac49c7aea..1c2d3fb98 100644 --- a/crates/kas-widgets/src/menu/menubar.rs +++ b/crates/kas-widgets/src/menu/menubar.rs @@ -6,9 +6,10 @@ //! Menubar use super::{Menu, SubMenu, SubMenuBuilder}; -use crate::IndexedList; use kas::event::{self, Command}; +use kas::layout::{self, RowSetter, RowSolver, RulesSetter, RulesSolver}; use kas::prelude::*; +use kas::theme::FrameStyle; impl_scope! { /// A menu-bar @@ -16,14 +17,13 @@ impl_scope! { /// This widget houses a sequence of menu buttons, allowing input actions across /// menus. #[autoimpl(Debug where D: trait)] - #[widget{ - layout = single; - }] + #[widget] pub struct MenuBar { #[widget_core] core: CoreData, - #[widget] - pub bar: IndexedList>, + direction: D, + widgets: Vec>, + layout_store: layout::DynRowStorage, delayed_open: Option, } @@ -46,7 +46,9 @@ impl_scope! { } MenuBar { core: Default::default(), - bar: IndexedList::new_with_direction(direction, menus), + direction, + widgets: menus, + layout_store: Default::default(), delayed_open: None, } } @@ -56,6 +58,50 @@ impl_scope! { } } + impl WidgetChildren for Self { + #[inline] + fn num_children(&self) -> usize { + self.widgets.len() + } + #[inline] + fn get_child(&self, index: usize) -> Option<&dyn WidgetConfig> { + self.widgets.get(index).map(|w| w.as_widget()) + } + #[inline] + fn get_child_mut(&mut self, index: usize) -> Option<&mut dyn WidgetConfig> { + self.widgets.get_mut(index).map(|w| w.as_widget_mut()) + } + } + + impl Layout for Self { + fn layout(&mut self) -> layout::Layout<'_> { + layout::Layout::slice(&mut self.widgets, self.direction, &mut self.layout_store) + } + + fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + let dim = (self.direction, self.widgets.len()); + let mut solver = RowSolver::new(axis, dim, &mut self.layout_store); + let frame_rules = mgr.frame(FrameStyle::MenuEntry, axis); + for (n, child) in self.widgets.iter_mut().enumerate() { + solver.for_child(&mut self.layout_store, n, |axis| { + let rules = child.size_rules(mgr.re(), axis); + frame_rules.surround_as_margin(rules).0 + }); + } + solver.finish(&mut self.layout_store) + } + + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { + self.core_data_mut().rect = rect; + let dim = (self.direction, self.widgets.len()); + let mut setter = RowSetter::, _>::new(rect, dim, align, &mut self.layout_store); + + for (n, child) in self.widgets.iter_mut().enumerate() { + child.set_rect(mgr, setter.child_rect(&mut self.layout_store, n), AlignHints::CENTER); + } + } + } + impl event::Handler for MenuBar { type Msg = M; @@ -76,7 +122,7 @@ impl_scope! { } => { if start_id.as_ref().map(|id| self.is_ancestor_of(id)).unwrap_or(false) { if source.is_primary() { - let any_menu_open = self.bar.iter().any(|w| w.menu_is_open()); + let any_menu_open = self.widgets.iter().any(|w| w.menu_is_open()); let press_in_the_bar = self.rect().contains(coord); if !press_in_the_bar || !any_menu_open { @@ -85,7 +131,7 @@ impl_scope! { mgr.set_grab_depress(source, start_id.clone()); if press_in_the_bar { if self - .bar + .widgets .iter() .any(|w| w.eq_id(&start_id) && !w.menu_is_open()) { @@ -116,7 +162,7 @@ impl_scope! { None => return Response::Used, }; - if self.bar.is_strict_ancestor_of(&id) { + if self.is_strict_ancestor_of(&id) { // We instantly open a sub-menu on motion over the bar, // but delay when over a sub-menu (most intuitive?) if self.rect().contains(coord) { @@ -152,17 +198,17 @@ impl_scope! { // Arrow keys can switch to the next / previous menu // as well as to the first / last item of an open menu. use Command::{Left, Up}; - let is_vert = self.bar.direction().is_vertical(); - let reverse = self.bar.direction().is_reversed() ^ matches!(cmd, Left | Up); + let is_vert = self.direction.is_vertical(); + let reverse = self.direction.is_reversed() ^ matches!(cmd, Left | Up); match cmd.as_direction().map(|d| d.is_vertical()) { Some(v) if v == is_vert => { - for i in 0..self.bar.len() { - if self.bar[i].menu_is_open() { + for i in 0..self.widgets.len() { + if self.widgets[i].menu_is_open() { let mut j = isize::conv(i); j = if reverse { j - 1 } else { j + 1 }; - j = j.rem_euclid(self.bar.len().cast()); - self.bar[i].set_menu_path(mgr, None, true); - let w = &mut self.bar[usize::conv(j)]; + j = j.rem_euclid(self.widgets.len().cast()); + self.widgets[i].set_menu_path(mgr, None, true); + let w = &mut self.widgets[usize::conv(j)]; w.set_menu_path(mgr, Some(&w.id()), true); break; } @@ -184,30 +230,35 @@ impl_scope! { impl event::SendEvent for Self { fn send(&mut self, mgr: &mut EventMgr, id: WidgetId, event: Event) -> Response { if self.eq_id(&id) { - self.handle(mgr, event) - } else { - match self.bar.send(mgr, id.clone(), event.clone()) { - Response::Unused => self.handle(mgr, event), - r => r.try_into().unwrap_or_else(|(_, msg)| { - log::trace!( - "Received by {} from {}: {:?}", - self.id(), - id, - kas::util::TryFormat(&msg) - ); - Response::Msg(msg) - }), + return self.handle(mgr, event); + } else if let Some(index) = id.next_key_after(self.id_ref()) { + if let Some(widget) = self.widgets.get_mut(index) { + return match widget.send(mgr, id.clone(), event.clone()) { + Response::Unused => self.handle(mgr, event), + r => r.try_into().unwrap_or_else(|msg| { + log::trace!( + "Received by {} from {}: {:?}", + self.id(), + id, + kas::util::TryFormat(&msg) + ); + Response::Msg(msg) + }), + }; } } + + debug_assert!(false, "SendEvent::send: bad WidgetId"); + Response::Unused } } - impl Menu for Self { + impl Self { fn set_menu_path(&mut self, mgr: &mut EventMgr, target: Option<&WidgetId>, set_focus: bool) { log::trace!("{}::set_menu_path: target={:?}, set_focus={}", self.identify(), target, set_focus); self.delayed_open = None; - for i in 0..self.bar.len() { - self.bar[i].set_menu_path(mgr, target, set_focus); + for i in 0..self.widgets.len() { + self.widgets[i].set_menu_path(mgr, target, set_focus); } } } diff --git a/crates/kas-widgets/src/menu/submenu.rs b/crates/kas-widgets/src/menu/submenu.rs index d5af2bc78..832d4eafc 100644 --- a/crates/kas-widgets/src/menu/submenu.rs +++ b/crates/kas-widgets/src/menu/submenu.rs @@ -5,12 +5,14 @@ //! Sub-menu -use super::{BoxedMenu, Menu}; -use crate::{Column, PopupFrame}; +use super::{BoxedMenu, Menu, SubItems}; +use crate::PopupFrame; +use kas::component::{Component, Label, Mark}; use kas::event::{self, Command}; +use kas::layout::{self, RulesSetter, RulesSolver}; use kas::prelude::*; -use kas::theme::{FrameStyle, TextClass}; -use kas::{layout, WindowId}; +use kas::theme::{FrameStyle, MarkStyle, TextClass}; +use kas::WindowId; impl_scope! { /// A sub-menu @@ -21,11 +23,10 @@ impl_scope! { core: CoreData, direction: D, pub(crate) key_nav: bool, - label: Text, - label_store: layout::TextStorage, - frame_store: layout::FrameStorage, + label: Label, + mark: Mark, #[widget] - pub list: PopupFrame>>, + list: PopupFrame>>, popup_id: Option, } @@ -58,16 +59,19 @@ impl_scope! { impl Self { /// Construct a sub-menu + /// + /// The sub-menu is opened in the `direction` given (contents are always vertical). #[inline] - pub fn new_with_direction>(direction: D, label: S, list: Vec>) -> Self { + pub fn new_with_direction>( + direction: D, label: S, list: Vec> + ) -> Self { SubMenu { core: Default::default(), direction, key_nav: true, - label: Text::new_single(label.into()), - label_store: Default::default(), - frame_store: Default::default(), - list: PopupFrame::new(Column::new(list)), + label: Label::new(label.into(), TextClass::MenuLabel), + mark: Mark::new(MarkStyle::Point(direction.as_direction())), + list: PopupFrame::new(MenuView::new(list)), popup_id: None, } } @@ -93,9 +97,8 @@ impl_scope! { fn handle_dir_key(&mut self, mgr: &mut EventMgr, cmd: Command) -> Response { if self.menu_is_open() { if let Some(dir) = cmd.as_direction() { - if dir.is_vertical() == self.list.direction().is_vertical() { - let rev = dir.is_reversed() ^ self.list.direction().is_reversed(); - mgr.next_nav_focus(self, rev, true); + if dir.is_vertical() { + mgr.next_nav_focus(self, false, true); Response::Used } else if dir == self.direction.as_direction().reversed() { self.close_menu(mgr, true); @@ -123,7 +126,7 @@ impl_scope! { impl WidgetConfig for Self { fn configure_recurse(&mut self, mgr: &mut SetRectMgr, id: WidgetId) { self.core_data_mut().id = id; - mgr.add_accel_keys(self.id_ref(), self.label.text().keys()); + mgr.add_accel_keys(self.id_ref(), self.label.keys()); mgr.new_accel_layer(self.id(), true); let id = self.id_ref().make_child(widget_index![self.list]); @@ -139,8 +142,7 @@ impl_scope! { impl kas::Layout for Self { fn layout(&mut self) -> layout::Layout<'_> { - let label = layout::Layout::text(&mut self.label_store, &mut self.label, TextClass::MenuLabel); - layout::Layout::frame(&mut self.frame_store, label, FrameStyle::MenuEntry) + layout::Layout::component(&mut self.label) } fn spatial_nav(&mut self, _: &mut SetRectMgr, _: bool, _: Option) -> Option { @@ -150,11 +152,10 @@ impl_scope! { fn draw(&mut self, mut draw: DrawMgr) { draw.frame(&*self, FrameStyle::MenuEntry, Default::default()); - draw.text_effects( - kas::theme::IdCoord(self.id_ref(), self.label_store.pos), - &self.label, - TextClass::MenuLabel, - ); + self.label.draw(draw.re(), &self.core.id); + if self.mark.rect.size != Size::ZERO { + self.mark.draw(draw, &self.core.id); + } } } @@ -203,6 +204,14 @@ impl_scope! { } impl Menu for Self { + fn sub_items(&mut self) -> Option { + Some(SubItems { + label: Some(&mut self.label), + submenu: Some(&mut self.mark), + ..Default::default() + }) + } + fn menu_is_open(&self) -> bool { self.popup_id.is_some() } @@ -232,15 +241,218 @@ impl_scope! { self.label.as_str() } } +} + +const MENU_VIEW_COLS: u32 = 5; +const fn menu_view_row_info(row: u32) -> layout::GridChildInfo { + layout::GridChildInfo { + col: 0, + col_end: MENU_VIEW_COLS, + row, + row_end: row + 1, + } +} + +impl_scope! { + /// A menu view + #[autoimpl(Debug)] + #[widget { + msg=::Msg; + }] + struct MenuView { + #[widget_core] + core: CoreData, + dim: layout::GridDimensions, + store: layout::DynGridStorage, //NOTE(opt): number of columns is fixed + list: Vec, + } + + impl kas::WidgetChildren for Self { + #[inline] + fn num_children(&self) -> usize { + self.list.len() + } + #[inline] + fn get_child(&self, index: usize) -> Option<&dyn WidgetConfig> { + self.list.get(index).map(|w| w.as_widget()) + } + #[inline] + fn get_child_mut(&mut self, index: usize) -> Option<&mut dyn WidgetConfig> { + self.list.get_mut(index).map(|w| w.as_widget_mut()) + } + } + + impl kas::Layout for Self { + fn size_rules(&mut self, mgr: SizeMgr, axis: AxisInfo) -> SizeRules { + self.dim = layout::GridDimensions { + cols: MENU_VIEW_COLS, + col_spans: self.list.iter_mut().filter_map(|w| w.sub_items().is_none().then(|| ())).count().cast(), + rows: self.list.len().cast(), + row_spans: 0, + }; + + let store = &mut self.store; + let mut solver = layout::GridSolver::, Vec<_>, _>::new(axis, self.dim, store); + + let frame_rules = mgr.frame(FrameStyle::MenuEntry, axis); + let is_horiz = axis.is_horizontal(); + let with_frame_rules = |rules| if is_horiz { + frame_rules.surround_as_margin(rules).0 + } else { + frame_rules.surround_no_margin(rules).0 + }; + + for (row, child) in self.list.iter_mut().enumerate() { + let row = u32::conv(row); + if let Some(items) = child.sub_items() { + if let Some(w) = items.toggle { + let info = layout::GridChildInfo::new(0, row); + solver.for_child(store, info, |axis| with_frame_rules(w.size_rules(mgr.re(), axis))); + } + if let Some(w) = items.icon { + let info = layout::GridChildInfo::new(1, row); + solver.for_child(store, info, |axis| with_frame_rules(w.size_rules(mgr.re(), axis))); + } + if let Some(w) = items.label { + let info = layout::GridChildInfo::new(2, row); + solver.for_child(store, info, |axis| with_frame_rules(w.size_rules(mgr.re(), axis))); + } + if let Some(w) = items.label2 { + let info = layout::GridChildInfo::new(3, row); + solver.for_child(store, info, |axis| with_frame_rules(w.size_rules(mgr.re(), axis))); + } + if let Some(w) = items.submenu { + let info = layout::GridChildInfo::new(4, row); + solver.for_child(store, info, |axis| with_frame_rules(w.size_rules(mgr.re(), axis))); + } + } else { + let info = menu_view_row_info(row); + solver.for_child(store, info, |axis| child.size_rules(mgr.re(), axis)); + } + } + solver.finish(store) + } + + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { + self.core.rect = rect; + let store = &mut self.store; + let mut setter = layout::GridSetter::, Vec<_>, _>::new(rect, self.dim, align, store); + + // Assumption: frame inner margin is at least as large as content margins + let dir = Direction::Right; // assumption: horiz and vert are the same + let frame_rules = mgr.size_mgr().frame(FrameStyle::MenuEntry, dir); + let (_, frame_offset, frame_size) = frame_rules.surround_no_margin(SizeRules::EMPTY); + let subtract_frame = |mut rect: Rect| { + rect.pos += Offset::splat(frame_offset); + rect.size -= Size::splat(frame_size); + rect + }; + + for (row, child) in self.list.iter_mut().enumerate() { + let row = u32::conv(row); + let child_rect = setter.child_rect(store, menu_view_row_info(row)); + + if let Some(items) = child.sub_items() { + if let Some(w) = items.toggle { + let info = layout::GridChildInfo::new(0, row); + w.set_rect(mgr, subtract_frame(setter.child_rect(store, info)), align); + } + if let Some(w) = items.icon { + let info = layout::GridChildInfo::new(1, row); + w.set_rect(mgr, subtract_frame(setter.child_rect(store, info)), align); + } + if let Some(w) = items.label { + let info = layout::GridChildInfo::new(2, row); + w.set_rect(mgr, subtract_frame(setter.child_rect(store, info)), align); + } + if let Some(w) = items.label2 { + let info = layout::GridChildInfo::new(3, row); + w.set_rect(mgr, subtract_frame(setter.child_rect(store, info)), align); + } + if let Some(w) = items.submenu { + let info = layout::GridChildInfo::new(4, row); + w.set_rect(mgr, subtract_frame(setter.child_rect(store, info)), align); + } + + child.core_data_mut().rect = child_rect; + } else { + child.set_rect(mgr, child_rect, align); + } + } + } + + fn find_id(&mut self, coord: Coord) -> Option { + if !self.rect().contains(coord) { + return None; + } + + for child in self.list.iter_mut() { + if let Some(id) = child.find_id(coord) { + return Some(id); + } + } + Some(self.id()) + } + + fn draw(&mut self, mut draw: DrawMgr) { + for child in self.list.iter_mut() { + child.draw(draw.re()); + } + } + } - impl SetAccel for Self { - fn set_accel_string(&mut self, string: AccelString) -> TkAction { - let mut action = TkAction::empty(); - if self.label.text().keys() != string.keys() { - action |= TkAction::RECONFIGURE; + impl event::SendEvent for Self { + fn send(&mut self, mgr: &mut EventMgr, id: WidgetId, event: Event) -> Response { + if let Some(index) = self.find_child_index(&id) { + if let Some(child) = self.list.get_mut(index) { + let r = child.send(mgr, id.clone(), event); + return match Response::try_from(r) { + Ok(r) => r, + Err(msg) => { + log::trace!( + "Received by {} from {}: {:?}", + self.id(), + id, + kas::util::TryFormat(&msg) + ); + Response::Msg(msg) + } + }; + } + } + + Response::Unused + } + } + + impl Self { + /// Construct from a list of menu items + pub fn new(list: Vec) -> Self { + MenuView { + core: Default::default(), + dim: Default::default(), + store: Default::default(), + list, } - let avail = self.core.rect.size.clamped_sub(self.frame_store.size); - action | kas::text::util::set_text_and_prepare(&mut self.label, string, avail) + } + + /// Number of menu items + pub fn len(&self) -> usize { + self.list.len() + } + } + + impl std::ops::Index for Self { + type Output = W; + + fn index(&self, index: usize) -> &Self::Output { + &self.list[index] + } + } + + impl std::ops::IndexMut for Self { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.list[index] } } } diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index 875a37266..a027d80ca 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -35,16 +35,10 @@ impl_scope! { size_mgr.text_bound(&mut self.text, TextClass::LabelScroll, axis) } - fn set_rect(&mut self, _: &mut SetRectMgr, rect: Rect, align: AlignHints) { + fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { self.core.rect = rect; - let size = rect.size; - self.required = self - .text - .update_env(|env| { - env.set_align(align.unwrap_or(Align::Default, Align::Default)); - env.set_bounds(size.cast()); - }) - .into(); + let align = align.unwrap_or(Align::Default, Align::Default); + self.required = mgr.text_set_size(&mut self.text, TextClass::LabelScroll, rect.size, align); self.set_view_offset_from_edit_pos(); } diff --git a/crates/kas-widgets/src/scrollbar.rs b/crates/kas-widgets/src/scrollbar.rs index 206e2b97e..4bd29a376 100644 --- a/crates/kas-widgets/src/scrollbar.rs +++ b/crates/kas-widgets/src/scrollbar.rs @@ -210,7 +210,6 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules { let (size, min_len) = size_mgr.scrollbar(); - self.min_handle_len = size.0; let margins = (0, 0); if self.direction.is_vertical() == axis.is_vertical() { SizeRules::new(min_len, min_len, margins, Stretch::High) @@ -228,6 +227,7 @@ impl_scope! { .aligned_rect(ideal_size, rect); self.core.rect = rect; self.handle.set_rect(mgr, rect, align); + self.min_handle_len = (mgr.size_mgr().scrollbar().0).0; let _ = self.update_handle(); } diff --git a/crates/kas-widgets/src/separator.rs b/crates/kas-widgets/src/separator.rs index 50452c9b0..ad66744c0 100644 --- a/crates/kas-widgets/src/separator.rs +++ b/crates/kas-widgets/src/separator.rs @@ -7,7 +7,7 @@ use std::fmt::Debug; -use crate::Menu; +use crate::menu::Menu; use kas::prelude::*; impl_scope! { diff --git a/crates/kas-widgets/src/splitter.rs b/crates/kas-widgets/src/splitter.rs index ef1a02c6d..d7790fe55 100644 --- a/crates/kas-widgets/src/splitter.rs +++ b/crates/kas-widgets/src/splitter.rs @@ -161,9 +161,6 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules { - // Assumption: if size_rules is called, then set_rect will be too. - self.size_solved = true; - if self.widgets.is_empty() { return SizeRules::EMPTY; } @@ -195,6 +192,7 @@ impl_scope! { fn set_rect(&mut self, mgr: &mut SetRectMgr, rect: Rect, align: AlignHints) { self.core.rect = rect; + self.size_solved = true; if self.widgets.is_empty() { return; } diff --git a/crates/kas-widgets/src/stack.rs b/crates/kas-widgets/src/stack.rs index eaa967b15..214eae356 100644 --- a/crates/kas-widgets/src/stack.rs +++ b/crates/kas-widgets/src/stack.rs @@ -40,7 +40,7 @@ impl_scope! { core: CoreData, align_hints: AlignHints, widgets: Vec, - sized_range: Range, + sized_range: Range, // range of pages for which size rules are solved active: usize, size_limit: usize, next: usize, diff --git a/crates/kas-widgets/src/view/driver.rs b/crates/kas-widgets/src/view/driver.rs index 3d8be02c9..db607a14e 100644 --- a/crates/kas-widgets/src/view/driver.rs +++ b/crates/kas-widgets/src/view/driver.rs @@ -43,8 +43,7 @@ pub trait Driver: Debug + 'static { /// Set the viewed data /// /// The widget may expect `configure` to be called at least once before data - /// is set and to have `size_rules` and `set_rect` called after each time - /// data is set. + /// is set and to have `set_rect` called after each time data is set. fn set(&self, widget: &mut Self::Widget, data: T) -> TkAction; /// Get data from the view /// diff --git a/examples/gallery.rs b/examples/gallery.rs index 6afe433eb..bcc72d023 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -130,7 +130,7 @@ fn main() -> Result<(), Box> { Quit, } - let menubar = MenuBar::::builder() + let menubar = menu::MenuBar::::builder() .menu("&App", |menu| { menu.entry("&Quit", Menu::Quit); })