diff --git a/Cargo.toml b/Cargo.toml index 30bd82efd..6d53672a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ stable = ["default", "x11", "serde", "toml", "yaml", "json", "ron", "macros_log" # Enables all "recommended" features for nightly rustc nightly = ["stable", "nightly-diagnostics", "kas-core/nightly"] # Additional, less recommendation-worthy features -experimental = ["dark-light", "recursive-layout-widgets", "unsafe_node"] +experimental = ["dark-light", "unsafe_node"] # Enable dynamic linking (faster linking via an extra run-time dependency): dynamic = ["dep:kas-dylib"] @@ -114,21 +114,6 @@ wayland = ["kas-core/wayland"] # Support X11 x11 = ["kas-core/x11"] -# Optimize generated layout widgets -# -# Recursive layout macros allow the generation of complex layout widgets; for -# example `row!["a", column!["b", "c"]]` yields a single layout widget (over -# three `StrLabel` widgets) instead of two separate layout widgets. -# (Note that this happens anyway in custom widget layout syntax where it is -# requried to support reference to widget fields.) -# -# A limited number of method calls such as `.align(AlignHints::LEFT)` and -# `.map_any()` are supported but are not linked correctly via rust-analyzer. -# -# Often results in unused import warnings, hence you may want to use -# RUSTFLAGS="-A unused_imports". -recursive-layout-widgets = ["kas-core/recursive-layout-widgets"] - # Optimize Node using unsafe code unsafe_node = ["kas-core/unsafe_node"] diff --git a/crates/kas-core/Cargo.toml b/crates/kas-core/Cargo.toml index dadbcb2cb..baf0f4ca2 100644 --- a/crates/kas-core/Cargo.toml +++ b/crates/kas-core/Cargo.toml @@ -25,7 +25,7 @@ stable = ["minimal", "clipboard", "markdown", "shaping", "spawn", "x11", "serde" # Enables all "recommended" features for nightly rustc nightly = ["stable", "nightly-diagnostics"] # Additional, less recommendation-worthy features -experimental = ["dark-light", "recursive-layout-widgets", "unsafe_node"] +experimental = ["dark-light", "unsafe_node"] # Enables better proc-macro diagnostics (including warnings); nightly only. nightly-diagnostics = ["kas-macros/nightly"] @@ -85,9 +85,6 @@ dark-light = ["dep:dark-light"] # Support spawning async tasks spawn = ["dep:async-global-executor"] -# Optimize generated layout widgets -recursive-layout-widgets = ["kas-macros/recursive-layout-widgets"] - # Optimize Node using unsafe code unsafe_node = [] diff --git a/crates/kas-core/src/core/layout.rs b/crates/kas-core/src/core/layout.rs index 349370c6b..91991edd8 100644 --- a/crates/kas-core/src/core/layout.rs +++ b/crates/kas-core/src/core/layout.rs @@ -264,10 +264,9 @@ pub trait Tile: Layout { /// Get the widget's region, relative to its parent. /// - /// This method is implemented by the `#[widget]` macro. - fn rect(&self) -> Rect { - unimplemented!() // make rustdoc show that this is a provided method - } + /// This method is usually implemented by the `#[widget]` macro. + /// See also [`kas::widget_set_rect`]. + fn rect(&self) -> Rect; /// Get the name of the widget struct /// @@ -291,6 +290,8 @@ pub trait Tile: Layout { /// Access a child as a `dyn Tile` /// + /// This method returns `None` exactly when `index >= self.num_children()`. + /// /// This method is usually implemented automatically by the `#[widget]` /// macro. fn get_child(&self, index: usize) -> Option<&dyn Tile> { @@ -316,16 +317,21 @@ pub trait Tile: Layout { /// Controls Tab navigation order of children. /// This method should: /// - /// - Return `None` if there is no next child - /// - Determine the next child after `from` (if provided) or the whole - /// range, optionally in `reverse` order - /// - Ensure that the selected widget is addressable through - /// [`Tile::get_child`] + /// - Return `None` if there is no (next) navigable child + /// - In the case there are navigable children and `from == None`, return + /// the index of the first (or last if `reverse`) navigable child + /// - In the case there are navigable children and `from == Some(index)`, + /// it may be expected that `from` is the output of a previous call to + /// this method; the method should return the next (or previous if + /// `reverse`) navigable child (if any) + /// + /// The return value mut be `None` or `Some(index)` where + /// `self.get_child(index).is_some()` (see [`Tile::get_child`]). /// - /// Both `from` and the return value use the widget index, as used by - /// [`Tile::get_child`]. + /// It is not required that all children (all indices `i` for + /// `i < self.num_children()`) are returnable from this method. /// - /// Default implementation: + /// Default (macro generated) implementation: /// /// - Generated from `#[widget]`'s layout property, if used (not always possible!) /// - Otherwise, iterate through children in order of definition diff --git a/crates/kas-core/src/draw/color.rs b/crates/kas-core/src/draw/color.rs index 22feeb40f..c1026a1a3 100644 --- a/crates/kas-core/src/draw/color.rs +++ b/crates/kas-core/src/draw/color.rs @@ -329,7 +329,7 @@ impl From<[u8; 4]> for Rgba8Srgb { #[derive(Copy, Clone, Debug, Error)] pub enum ParseError { /// Incorrect input length - #[error("invalid length (expected 6 or 8 bytes")] + #[error("invalid length (expected 6 or 8 bytes)")] Length, /// Invalid hex byte #[error("invalid hex byte (expected 0-9, a-f or A-F)")] diff --git a/crates/kas-core/src/hidden.rs b/crates/kas-core/src/hidden.rs index 6def0ea81..1b839c4c5 100644 --- a/crates/kas-core/src/hidden.rs +++ b/crates/kas-core/src/hidden.rs @@ -10,9 +10,10 @@ //! not supported (i.e. **changes are not considered breaking**). use crate::event::ConfigCx; -use crate::geom::Rect; -use crate::layout::AlignHints; -use crate::theme::{Text, TextClass}; +use crate::geom::{Rect, Size}; +use crate::layout::{AlignHints, AxisInfo, SizeRules}; +use crate::theme::{SizeCx, Text, TextClass}; +#[allow(unused)] use crate::Action; use crate::{Events, Layout, Widget}; use kas_macros::{autoimpl, impl_scope, widget_set_rect}; @@ -94,3 +95,76 @@ impl_scope! { } } } + +impl_scope! { + /// Apply an alignment hint + /// + /// The inner widget chooses how to apply (or ignore) this hint. + /// + /// Usually, this type will be constructed through one of the methods on + /// [`AdaptWidget`](https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidget.html). + #[widget{ derive = self.inner; }] + pub struct Align { + pub inner: W, + /// Hints may be modified directly. + /// + /// Use [`Action::RESIZE`] to apply changes. + pub hints: AlignHints, + } + + impl Self { + /// Construct + #[inline] + pub fn new(inner: W, hints: AlignHints) -> Self { + Align { inner, hints } + } + } + + impl Layout for Self { + fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) { + self.inner.set_rect(cx, rect, self.hints.combine(hints)); + } + } +} + +impl_scope! { + /// Apply an alignment hint, squash and align the result + /// + /// The inner widget chooses how to apply (or ignore) this hint. + /// The widget is then prevented from stretching beyond its ideal size, + /// aligning within the available rect. + /// + /// Usually, this type will be constructed through one of the methods on + /// [`AdaptWidget`](https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidget.html). + #[widget{ derive = self.inner; }] + pub struct Pack { + pub inner: W, + /// Hints may be modified directly. + /// + /// Use [`Action::RESIZE`] to apply changes. + pub hints: AlignHints, + size: Size, + } + + impl Self { + /// Construct + #[inline] + pub fn new(inner: W, hints: AlignHints) -> Self { + Pack { inner, hints, size: Size::ZERO } + } + } + + impl Layout for Self { + fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules { + let rules = self.inner.size_rules(sizer, axis); + self.size.set_component(axis, rules.ideal_size()); + rules + } + + fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) { + let align = self.hints.combine(hints).complete_default(); + let rect = align.aligned_rect(self.size, rect); + self.inner.set_rect(cx, rect, hints); + } + } +} diff --git a/crates/kas-core/src/layout/align.rs b/crates/kas-core/src/layout/align.rs index de46dd6d0..3dc4aba7e 100644 --- a/crates/kas-core/src/layout/align.rs +++ b/crates/kas-core/src/layout/align.rs @@ -51,9 +51,9 @@ impl AlignHints { /// Top, left pub const TOP_LEFT: AlignHints = AlignHints::new(Some(Align::TL), Some(Align::TL)); /// Top, right - pub const TOP_RIGHT: AlignHints = AlignHints::new(Some(Align::TL), Some(Align::BR)); + pub const TOP_RIGHT: AlignHints = AlignHints::new(Some(Align::BR), Some(Align::TL)); /// Bottom, left - pub const BOTTOM_LEFT: AlignHints = AlignHints::new(Some(Align::BR), Some(Align::TL)); + pub const BOTTOM_LEFT: AlignHints = AlignHints::new(Some(Align::TL), Some(Align::BR)); /// Bottom, right pub const BOTTOM_RIGHT: AlignHints = AlignHints::new(Some(Align::BR), Some(Align::BR)); diff --git a/crates/kas-core/src/layout/visitor.rs b/crates/kas-core/src/layout/visitor.rs index 59c266000..8947a8700 100644 --- a/crates/kas-core/src/layout/visitor.rs +++ b/crates/kas-core/src/layout/visitor.rs @@ -78,11 +78,13 @@ impl<'a> Visitor> { storage: &'a mut FrameStorage, child: C, style: FrameStyle, + bg: Background, ) -> Visitor { Visitor(Frame { child, storage, style, + bg, }) } @@ -340,6 +342,7 @@ struct Frame<'a, C: Layout> { child: C, storage: &'a mut FrameStorage, style: FrameStyle, + bg: Background, } impl<'a, C: Layout> Layout for Frame<'a, C> { @@ -365,7 +368,7 @@ impl<'a, C: Layout> Layout for Frame<'a, C> { } fn draw(&mut self, mut draw: DrawCx) { - draw.frame(self.storage.rect, self.style, Background::Default); + draw.frame(self.storage.rect, self.style, self.bg); self.child.draw(draw); } } diff --git a/crates/kas-core/src/popup.rs b/crates/kas-core/src/popup.rs index 71c579eac..c9089592b 100644 --- a/crates/kas-core/src/popup.rs +++ b/crates/kas-core/src/popup.rs @@ -35,7 +35,7 @@ impl_scope! { /// /// A popup receives input data from its parent like any other widget. #[widget { - layout = frame!(self.inner, style = kas::theme::FrameStyle::Popup); + layout = frame!(self.inner).with_style(kas::theme::FrameStyle::Popup); }] pub struct Popup { core: widget_core!(), diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 3ecb509ac..fcd670063 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -7,7 +7,7 @@ use super::{FrameStyle, MarkStyle, SelectionStyle, SizeCx, Text, TextClass, ThemeSize}; use crate::dir::Direction; -use crate::draw::color::Rgb; +use crate::draw::color::{ParseError, Rgb}; use crate::draw::{Draw, DrawIface, DrawShared, DrawSharedImpl, ImageId, PassType}; use crate::event::{ConfigCx, EventState}; use crate::geom::{Offset, Rect}; @@ -29,6 +29,39 @@ pub enum Background { Rgb(Rgb), } +impl From for Background { + #[inline] + fn from(color: Rgb) -> Self { + Background::Rgb(color) + } +} + +#[derive(Copy, Clone, Debug, thiserror::Error)] +pub enum BackgroundParseError { + /// No `#` prefix + /// + /// NOTE: this exists to allow the possibility of supporting new exprs like + /// "Default" or "Error". + #[error("Unknown: no `#` prefix")] + Unknown, + /// Invalid hex + #[error("invalid hex sequence")] + InvalidRgb(#[from] ParseError), +} + +impl std::str::FromStr for Background { + type Err = BackgroundParseError; + + #[inline] + fn from_str(s: &str) -> Result { + if s.starts_with("#") { + Rgb::from_str(s).map(|c| c.into()).map_err(|e| e.into()) + } else { + Err(BackgroundParseError::Unknown) + } + } +} + /// Draw interface /// /// This interface is provided to widgets in [`crate::Layout::draw`]. diff --git a/crates/kas-core/src/theme/text.rs b/crates/kas-core/src/theme/text.rs index 88110d91a..1495cbb61 100644 --- a/crates/kas-core/src/theme/text.rs +++ b/crates/kas-core/src/theme/text.rs @@ -84,7 +84,7 @@ impl Layout for Text { } fn draw(&mut self, mut draw: DrawCx) { - draw.text(self.rect, &self); + draw.text(self.rect, self); } } diff --git a/crates/kas-macros/Cargo.toml b/crates/kas-macros/Cargo.toml index 3a2a6e79e..e1c21df81 100644 --- a/crates/kas-macros/Cargo.toml +++ b/crates/kas-macros/Cargo.toml @@ -19,9 +19,6 @@ proc-macro = true # Requires that all crates using these macros depend on the log crate. log = [] -# Optimize generated layout widgets -recursive-layout-widgets = [] - # Enable reporting of warnings from proc-macros nightly = ["proc-macro-error2/nightly"] diff --git a/crates/kas-macros/src/collection.rs b/crates/kas-macros/src/collection.rs index 92d5e3580..f7571513c 100644 --- a/crates/kas-macros/src/collection.rs +++ b/crates/kas-macros/src/collection.rs @@ -11,31 +11,17 @@ use syn::parse::{Error, Parse, ParseStream, Result}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::Comma; -use syn::{braced, parenthesized}; -use syn::{Expr, Ident, Lifetime, LitInt, LitStr, Token}; - -#[derive(Debug)] -pub enum StorIdent { - Named(Ident), - Generated(Ident), -} -impl From for StorIdent { - fn from(mut lt: Lifetime) -> StorIdent { - lt.ident.set_span(lt.span()); - StorIdent::Named(lt.ident) - } -} -impl From for StorIdent { - fn from(ident: Ident) -> StorIdent { - StorIdent::Generated(ident) - } -} -impl ToTokens for StorIdent { - fn to_tokens(&self, toks: &mut Toks) { - match self { - StorIdent::Named(ident) | StorIdent::Generated(ident) => ident.to_tokens(toks), - } - } +use syn::{braced, bracketed, parenthesized}; +use syn::{Expr, Ident, LitInt, LitStr, Token}; + +#[allow(non_camel_case_types)] +mod kw { + syn::custom_keyword!(align); + syn::custom_keyword!(pack); + syn::custom_keyword!(aligned_column); + syn::custom_keyword!(aligned_row); + syn::custom_keyword!(column); + syn::custom_keyword!(row); } #[derive(Default)] @@ -47,14 +33,6 @@ impl NameGenerator { let span = Span::call_site(); Ident::new(&name, span) } - - pub fn parse_or_next(&mut self, input: ParseStream) -> Result { - if input.peek(Lifetime) { - Ok(input.parse::()?.into()) - } else { - Ok(self.next().into()) - } - } } #[derive(Copy, Clone, Debug)] @@ -174,14 +152,41 @@ impl ToTokens for GridDimensions { } pub enum Item { - Label(Ident, LitStr), + Label(Ident, Toks, Toks), Widget(Ident, Expr), } impl Item { fn parse(input: ParseStream, gen: &mut NameGenerator) -> Result { if input.peek(LitStr) { - Ok(Item::Label(gen.next(), input.parse()?)) + let text: LitStr = input.parse()?; + let span = text.span(); + let mut ty = quote! { ::kas::hidden::StrLabel }; + let mut def = quote_spanned! {span=> ::kas::hidden::StrLabel::new(#text) }; + + if input.peek(Token![.]) && input.peek2(kw::align) { + let _: Token![.] = input.parse()?; + let _: kw::align = input.parse()?; + + let inner; + let _ = parenthesized!(inner in input); + let hints: Expr = inner.parse()?; + + ty = quote! { ::kas::hidden::Align<#ty> }; + def = quote! { ::kas::hidden::Align::new(#def, #hints) }; + } else if input.peek(Token![.]) && input.peek2(kw::pack) { + let _: Token![.] = input.parse()?; + let _: kw::pack = input.parse()?; + + let inner; + let _ = parenthesized!(inner in input); + let hints: Expr = inner.parse()?; + + ty = quote! { ::kas::hidden::Pack<#ty> }; + def = quote! { ::kas::hidden::Pack::new(#def, #hints) }; + } + + Ok(Item::Label(gen.next(), ty, def)) } else { Ok(Item::Widget(gen.next(), input.parse()?)) } @@ -211,33 +216,41 @@ impl Parse for Collection { } impl Parse for CellCollection { - fn parse(inner: ParseStream) -> Result { + fn parse(input: ParseStream) -> Result { + if input.peek(kw::aligned_column) { + let _: kw::aligned_column = input.parse()?; + return Self::parse_aligned::(input, false); + } else if input.peek(kw::aligned_row) { + let _: kw::aligned_row = input.parse()?; + return Self::parse_aligned::(input, true); + } + let mut gen = NameGenerator::default(); let mut cells = vec![]; let mut items = vec![]; - while !inner.is_empty() { - cells.push(inner.parse()?); - let _: Token![=>] = inner.parse()?; + while !input.is_empty() { + cells.push(input.parse()?); + let _: Token![=>] = input.parse()?; let item; let require_comma; - if inner.peek(syn::token::Brace) { - let inner2; - let _ = braced!(inner2 in inner); - item = Item::parse(&inner2, &mut gen)?; + if input.peek(syn::token::Brace) { + let inner; + let _ = braced!(inner in input); + item = Item::parse(&inner, &mut gen)?; require_comma = false; } else { - item = Item::parse(inner, &mut gen)?; + item = Item::parse(input, &mut gen)?; require_comma = true; } items.push(item); - if inner.is_empty() { + if input.is_empty() { break; } - if let Err(e) = inner.parse::() { + if let Err(e) = input.parse::() { if require_comma { return Err(e); } @@ -248,6 +261,46 @@ impl Parse for CellCollection { } } +impl CellCollection { + fn parse_aligned(input: ParseStream, transmute: bool) -> Result { + let mut gen = NameGenerator::default(); + let mut cells = vec![]; + let mut items = vec![]; + + let mut row = 0; + while !input.is_empty() { + let _: Kw = input.parse()?; + let _: Token![!] = input.parse()?; + + let inner; + let _ = bracketed!(inner in input); + let mut col = 0; + while !inner.is_empty() { + let (mut a, mut b) = (col, row); + if transmute { + (a, b) = (b, a); + } + cells.push(CellInfo::new(a, b)); + items.push(Item::parse(&inner, &mut gen)?); + + if inner.is_empty() { + break; + } + let _: Token![,] = inner.parse()?; + col += 1; + } + + if input.is_empty() { + break; + } + let _: Token![,] = input.parse()?; + row += 1; + } + + Ok(CellCollection(cells, Collection(items))) + } +} + impl Collection { pub fn impl_parts(&self) -> (Toks, Toks, Toks, Toks, Toks) { let mut data_ty = None; @@ -275,20 +328,13 @@ impl Collection { for (index, item) in self.0.iter().enumerate() { let path = match item { - Item::Label(stor, text) => { - let span = text.span(); + Item::Label(stor, ty, def) => { if let Some(ref data_ty) = data_ty { - stor_ty.append_all( - quote! { #stor: ::kas::hidden::MapAny<#data_ty, ::kas::hidden::StrLabel>, }, - ); - stor_def.append_all( - quote_spanned! {span=> #stor: ::kas::hidden::MapAny::new(::kas::hidden::StrLabel::new(#text)), }, - ); + stor_ty.append_all(quote! { #stor: ::kas::hidden::MapAny<#data_ty, #ty>, }); + stor_def.append_all(quote! { #stor: ::kas::hidden::MapAny::new(#def), }); } else { - stor_ty.append_all(quote! { #stor: ::kas::hidden::StrLabel, }); - stor_def.append_all( - quote_spanned! {span=> #stor: ::kas::hidden::StrLabel::new(#text), }, - ); + stor_ty.append_all(quote! { #stor: #ty, }); + stor_def.append_all(quote! { #stor: #def, }); } stor.to_token_stream() } diff --git a/crates/kas-macros/src/lib.rs b/crates/kas-macros/src/lib.rs index 68fd30882..c7cf93ae2 100644 --- a/crates/kas-macros/src/lib.rs +++ b/crates/kas-macros/src/lib.rs @@ -19,10 +19,10 @@ mod collection; mod extends; mod make_layout; mod scroll_traits; +mod visitors; mod widget; mod widget_args; mod widget_derive; -mod widget_index; /// Implement `Default` /// @@ -208,23 +208,13 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// implementation of `Tile::nav_next`, with a couple of exceptions /// (where macro-time analysis is insufficient to implement this method). /// -/// > [_Column_](macro@column), [_Row_](macro@row), [_List_](macro@list), [_AlignedColumn_](macro@aligned_column), [_AlignedRow_](macro@aligned_row), [_Grid_](macro@grid), [_Float_](macro@float) :\ +/// > [_Column_], [_Row_], [_List_] [_AlignedColumn_], [_AlignedRow_], [_Grid_], [_Float_], [_Frame_] :\ /// >    These stand-alone macros are explicitly supported in this position.\ -/// >    Optionally, a _Storage_ specifier is supported immediately after the macro name, e.g.\ -/// >    `column! 'storage_name ["one", "two"]` /// /// > _Single_ :\ /// >    `self` `.` _Member_\ /// >    A named child: `self.foo` (more precisely, this matches any expression starting `self`, and uses `&mut (#expr)`) /// > -/// > _Frame_ :\ -/// >    `frame!` _Storage_? `(` _Layout_ ( `,` `style` `=` _Expr_ )? `)`\ -/// >    Adds a frame of type _Expr_ around content, defaulting to `FrameStyle::Frame`. -/// > -/// > _Button_ :\ -/// >    `button!` _Storage_? `(` _Layout_ ( `,` `color` `=` _Expr_ )? `)`\ -/// >    Adds a button frame (optionally with color _Expr_) around content. -/// > /// > _WidgetConstructor_ :\ /// >    _Expr_\ /// >    An expression yielding a widget, e.g. `Label::new("Hello world")`. The result must be an object of some type `W: Widget`. @@ -233,24 +223,12 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// >    _StrLit_\ /// >    A string literal generates a label widget, e.g. "Hello world". This is an internal type without text wrapping. /// > -/// > _NonNavigable_ :\ -/// >    `non_navigable!` `(` _Layout_ `)` \ -/// >    Does not affect layout. Specifies that the content is excluded from tab-navigation order. -/// /// Additional syntax rules (not layout items): /// /// > _Member_ :\ /// >    _Ident_ | _Index_\ /// >    The name of a struct field or an index into a tuple struct. /// > -/// > _Direction_ :\ -/// >    `left` | `right` | `up` | `down` | _Expr_:\ -/// >    Note that an _Expr_ must start with `self` -/// > -/// > _Storage_ :\ -/// >    `'` _Ident_\ -/// >    Used to explicitly name the storage used by a generated widget or layout; for example `row 'x: ["A", "B", "C"]` will add a field `x: R` where `R: RowStorage` within the generated `widget_core!()`. If omitted, the field name will be anonymous (generated). -/// /// ## Examples /// /// A simple example is the @@ -261,7 +239,7 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// /// A frame around content /// #[derive(Clone, Default)] /// #[widget{ -/// layout = frame!(self.inner, style = kas::theme::FrameStyle::Frame); +/// layout = frame!(self.inner); /// }] /// pub struct Frame { /// core: widget_core!(), @@ -339,6 +317,14 @@ pub fn impl_scope(input: TokenStream) -> TokenStream { /// [`CursorIcon`]: https://docs.rs/kas/latest/kas/event/enum.CursorIcon.html /// [`IsUsed`]: https://docs.rs/kas/latest/kas/event/enum.IsUsed.html /// [`Deref`]: std::ops::Deref +/// [_Column_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.column.html +/// [_Row_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.row.html +/// [_List_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.list.html +/// [_Float_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.float.html +/// [_Frame_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.frame.html +/// [_Grid_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.grid.html +/// [_AlignedColumn_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.aligned_column.html +/// [_AlignedRow_]: https://docs.rs/kas-widgets/latest/kas_widgets/macro.aligned_row.html #[proc_macro_attribute] #[proc_macro_error] pub fn widget(_: TokenStream, item: TokenStream) -> TokenStream { @@ -420,7 +406,7 @@ pub fn impl_anon(input: TokenStream) -> TokenStream { #[proc_macro_error] #[proc_macro] pub fn widget_index(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as widget_index::UnscopedInput); + let input = parse_macro_input!(input as visitors::UnscopedInput); input.into_token_stream().into() } @@ -429,8 +415,11 @@ pub fn widget_index(input: TokenStream) -> TokenStream { /// Widgets have a hidden field of type [`Rect`] in their `widget_core!()`, used /// to implement method [`Tile::rect`]. This macro assigns to that field. /// -/// This macro is usable only within an [`impl_scope!`] macro using the -/// [`widget`](macro@widget) attribute. +/// This macro is usable only within the definition of `Layout::set_rect` within +/// an [`impl_scope!`] macro using the [`widget`](macro@widget) attribute. +/// +/// The method `Tile::rect` will be generated if this macro is used by the +/// widget, otherwise a definition of the method must be provided. /// /// Example usage: /// ```ignore @@ -444,223 +433,79 @@ pub fn widget_index(input: TokenStream) -> TokenStream { #[proc_macro_error] #[proc_macro] pub fn widget_set_rect(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as widget_index::UnscopedInput); + let input = parse_macro_input!(input as visitors::UnscopedInput); input.into_token_stream().into() } -trait ExpandLayout { - fn expand_layout(self, name: &str) -> TokenStream; -} -impl ExpandLayout for make_layout::Tree { - fn expand_layout(self, name: &str) -> TokenStream { - match self.expand_as_widget(name) { - Ok(toks) => toks.into(), - Err(err) => { - emit_call_site_error!(err); - TokenStream::default() - } - } - } -} - -/// Make a column widget -/// -/// Items support [widget layout syntax](macro@widget#layout-1). -/// -/// # Example -/// -/// ```ignore -/// let my_widget = kas::column! [ -/// "one", -/// "two", -/// ]; -/// ``` -#[proc_macro_error] -#[proc_macro] -pub fn column(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::column).expand_layout("_Column") -} - -/// Make a row widget -/// -/// Items support [widget layout syntax](macro@widget#layout-1). -/// -/// # Example -/// -/// ```ignore -/// let my_widget = kas::row! ["one", "two"]; -/// ``` -#[proc_macro_error] -#[proc_macro] -pub fn row(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::row).expand_layout("_Row") -} - -/// Make a list widget -/// -/// This is a more generic variant of [`column!`] and [`row!`]. -/// -/// Children are navigated in visual order. -/// -/// Items support [widget layout syntax](macro@widget#layout-1). -/// -/// # Example -/// -/// ```ignore -/// let my_widget = kas::list!(left, ["one", "two"]); -/// ``` +/// Generate an anonymous struct which implements [`kas::Collection`] /// /// # Syntax /// -/// > _List_ :\ -/// >    `list!` `(` _Direction_ `,` `[` ( _Layout_ `,` )* ( _Layout_ `,`? )? `]` `}` +/// > _Collection_ :\ +/// >    `collection!` `[` _Items_\? `]` /// > -/// > _Direction_ :\ -/// >    `left` | `right` | `up` | `down` - -#[proc_macro_error] -#[proc_macro] -pub fn list(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::list).expand_layout("_List") -} - -/// Make a float widget -/// -/// All children occupy the same space with the first child on top. -/// -/// Size is determined as the maximum required by any child for each axis. -/// All children are assigned this size. It is usually necessary to use [`pack`] -/// or a similar mechanism to constrain a child to avoid it hiding the content -/// underneath (note that even if an unconstrained child does not *visually* -/// hide everything beneath, it may still "occupy" the assigned area, preventing -/// mouse clicks from reaching the widget beneath). +/// > _Items_ :\ +/// >    (_Item_ `,`)\* _Item_ `,`\? /// -/// Children are navigated in order of declaration. +/// In this case, _Item_ may be: /// -/// Items support [widget layout syntax](macro@widget#layout-1). +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` /// -/// # Example +/// In case all _Item_ instances are a string literal, the data type of the +/// `collection!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. /// -/// ```ignore -/// let my_widget = kas::float! [ -/// "one".pack(AlignHints::TOP_LEFT), -/// "two".pack(AlignHints::BOTTOM_RIGHT), -/// "some text\nin the\nbackground" -/// ]; -/// ``` +/// For example usage, see [`List`](https://docs.rs/kas/latest/kas/widgets/struct.List.html). /// -/// [`pack`]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidget.html#method.pack +/// [`kas::Collection`]: https://docs.rs/kas/latest/kas/trait.Collection.html +/// [`align`]: https://docs.rs/kas/latest/kas/widgets/adapt/trait.AdaptWidget.html#method.align +/// [`pack`]: https://docs.rs/kas/latest/kas/widgets/adapt/trait.AdaptWidget.html#method.pack #[proc_macro_error] #[proc_macro] -pub fn float(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::float).expand_layout("_Float") +pub fn collection(input: TokenStream) -> TokenStream { + parse_macro_input!(input as collection::Collection) + .expand() + .into() } -/// Make a grid widget -/// -/// Constructs a table with auto-determined number of rows and columns. -/// Each child is assigned a cell using match-like syntax. -/// -/// A child may be stretched across multiple cells using range-like syntax: -/// `3..5`, `3..=4` and `3..+2` are all equivalent. -/// -/// Behaviour of overlapping widgets is identical to [`float!`]: the first -/// declared item is on top. -/// -/// Children are navigated in order of declaration. -/// -/// Items support [widget layout syntax](macro@widget#layout-1). -/// -/// # Example -/// -/// ```ignore -/// let my_widget = kas::grid! { -/// (0, 0) => "top left", -/// (1, 0) => "top right", -/// (0..2, 1) => "bottom row (merged)", -/// }; -/// ``` +/// Generate an anonymous struct which implements [`kas::CellCollection`] /// /// # Syntax /// -/// > _Grid_ :\ -/// >    `grid!` `{` _GridCell_* `}` +/// > _Collection_ :\ +/// >    `collection!` `[` _ItemArms_\? `]` /// > -/// > _GridCell_ :\ -/// >    `(` _CellRange_ `,` _CellRange_ `)` `=>` ( _Layout_ | `{` _Layout_ `}` ) +/// > _ItemArms_ :\ +/// >    (_ItemArm_ `,`)\* _ItemArm_ `,`\? /// > -/// > _CellRange_ :\ -/// >    _LitInt_ ( `..` `+`? _LitInt_ )? -/// -/// Cells are specified using `match`-like syntax from `(col_spec, row_spec)` to -/// a layout, e.g.: `(1, 0) => self.foo`. Spans are specified via range syntax, -/// e.g. `(0..2, 1) => self.bar`. -#[proc_macro_error] -#[proc_macro] -pub fn grid(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::grid).expand_layout("_Grid") -} - -/// Make an aligned column widget -/// -/// Items support [widget layout syntax](macro@widget#layout-1). -/// -/// # Example -/// -/// ```ignore -/// let my_widget = kas::aligned_column! [ -/// row!["one", "two"], -/// row!["three", "four"], -/// ]; -/// ``` -#[proc_macro_error] -#[proc_macro] -pub fn aligned_column(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::aligned_column).expand_layout("_AlignedColumn") -} - -/// Make an aligned row widget -/// -/// Items support [widget layout syntax](macro@widget#layout-1). -/// -/// # Example -/// -/// ```ignore -/// let my_widget = kas::aligned_row! [ -/// column!["one", "two"], -/// column!["three", "four"], -/// ]; -/// ``` -#[proc_macro_error] -#[proc_macro] -pub fn aligned_row(input: TokenStream) -> TokenStream { - parse_macro_input!(input with make_layout::Tree::aligned_row).expand_layout("_AlignedRow") -} - -/// Generate an anonymous struct which implements [`kas::Collection`] +/// > _ItemArm_ :\ +/// >    `(` _Column_ `,` _Row_ `)` `=>` _Item_ +/// > +/// > _Column_, _Row_ :\ +/// >    _LitInt_ | ( _LitInt_ `..` `+` _LitInt_ ) | ( _LitInt_ `..` +/// > _LitInt_ ) | ( _LitInt_ `..=` _LitInt_ ) /// -/// Each item must be either a string literal (inferred as a static label) or a -/// widget (implements [`kas::Widget`](https://docs.rs/kas/latest/kas/trait.Widget.html)). +/// Here, _Column_ and _Row_ are selected via an index (from 0), a range of +/// indices, or a start + increment. For example, `2` = `2..+1` = `2..3` = +/// `2..=2` while `5..+2` = `5..7` = `5..=6`. /// -/// For example usage, see [`List`](https://docs.rs/kas/latest/kas/widgets/struct.List.html). +/// _Item_ may be: /// -/// [`kas::Collection`]: https://docs.rs/kas/latest/kas/trait.Collection.html -#[proc_macro_error] -#[proc_macro] -pub fn collection(input: TokenStream) -> TokenStream { - parse_macro_input!(input as collection::Collection) - .expand() - .into() -} - -/// Generate an anonymous struct which implements [`kas::CellCollection`] +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` /// -/// Each item must be either a string literal (inferred as a static label) or a -/// widget (implements [`kas::Widget`](https://docs.rs/kas/latest/kas/trait.Widget.html)). +/// In case all _Item_ instances are a string literal, the data type of the +/// `collection!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. /// /// For example usage, see [`Grid`](https://docs.rs/kas/latest/kas/widgets/struct.Grid.html). /// /// [`kas::CellCollection`]: https://docs.rs/kas/latest/kas/trait.CellCollection.html +/// [`align`]: https://docs.rs/kas/latest/kas/widgets/adapt/trait.AdaptWidget.html#method.align +/// [`pack`]: https://docs.rs/kas/latest/kas/widgets/adapt/trait.AdaptWidget.html#method.pack #[proc_macro_error] #[proc_macro] pub fn cell_collection(input: TokenStream) -> TokenStream { diff --git a/crates/kas-macros/src/make_layout.rs b/crates/kas-macros/src/make_layout.rs index 9a8a6198f..a098bbf57 100644 --- a/crates/kas-macros/src/make_layout.rs +++ b/crates/kas-macros/src/make_layout.rs @@ -3,8 +3,7 @@ // You may obtain a copy of the License in the LICENSE-APACHE file or at: // https://www.apache.org/licenses/LICENSE-2.0 -use crate::collection::{CellInfo, GridDimensions, NameGenerator, StorIdent}; -use crate::widget; +use crate::collection::{CellInfo, GridDimensions, NameGenerator}; use crate::widget_args::{Child, ChildIdent}; use proc_macro2::{Span, TokenStream as Toks}; use proc_macro_error2::emit_error; @@ -29,7 +28,6 @@ mod kw { custom_keyword!(center); custom_keyword!(stretch); custom_keyword!(frame); - custom_keyword!(button); custom_keyword!(list); custom_keyword!(grid); custom_keyword!(default); @@ -38,12 +36,12 @@ mod kw { custom_keyword!(aligned_column); custom_keyword!(aligned_row); custom_keyword!(float); - custom_keyword!(non_navigable); custom_keyword!(px); custom_keyword!(em); - custom_keyword!(style); - custom_keyword!(color); custom_keyword!(map_any); + custom_keyword!(with_direction); + custom_keyword!(with_style); + custom_keyword!(with_background); } #[derive(Default)] @@ -67,7 +65,7 @@ impl Tree { self.0.generate(core_path) } - /// Generate implementation of nav_next (excludes fn signature) + /// Generate implementation of nav_next pub fn nav_next<'a, I: Clone + Iterator>( &self, children: I, @@ -75,242 +73,24 @@ impl Tree { let mut v = Vec::new(); self.0.nav_next(children, &mut v).map(|()| { quote! { - let mut iter = [#(#v),*].into_iter(); - if !reverse { - if let Some(wi) = from { - let _ = iter.find(|x| *x == wi); - } - iter.next() - } else { - let mut iter = iter.rev(); - if let Some(wi) = from { - let _ = iter.find(|x| *x == wi); + fn nav_next(&self, reverse: bool, from: Option) -> Option { + let mut iter = [#(#v),*].into_iter(); + if !reverse { + if let Some(wi) = from { + let _ = iter.find(|x| *x == wi); + } + iter.next() + } else { + let mut iter = iter.rev(); + if let Some(wi) = from { + let _ = iter.find(|x| *x == wi); + } + iter.next() } - iter.next() } } }) } - - /// Synthesize an entire widget from the layout - pub fn expand_as_widget(self, widget_name: &str) -> Result { - let mut children = Vec::new(); - let data_ty: syn::Type = syn::parse_quote! { _Data }; - let stor_defs = self.storage_fields(&mut children, &data_ty); - let stor_ty = &stor_defs.ty_toks; - let stor_def = &stor_defs.def_toks; - - let name = Ident::new(widget_name, Span::call_site()); - let core_path = quote! { self }; - let (impl_generics, impl_target) = if stor_defs.used_data_ty { - (quote! { <_Data> }, quote! { #name <_Data> }) - } else { - (quote! {}, quote! { #name }) - }; - - let count = children.len(); - let num_children = quote! { - fn num_children(&self) -> usize { - #count - } - }; - - let mut get_rules = quote! {}; - for (index, child) in children.iter().enumerate() { - get_rules.append_all(child.ident.get_rule(&core_path, index)); - } - - let core_impl = widget::impl_core_methods(widget_name, &core_path); - let widget_impl = widget::impl_widget( - &impl_generics, - &impl_target, - &data_ty, - &core_path, - &children, - true, - ); - - let layout_visitor = self.layout_visitor(&core_path)?; - let nav_next = match self.nav_next(children.iter()) { - Ok(result) => Some(result), - Err((span, msg)) => { - emit_error!(span, "unable to generate `fn Tile::nav_next`: {}", msg); - None - } - }; - - let toks = quote! {{ - struct #name #impl_generics { - _rect: ::kas::geom::Rect, - _id: ::kas::Id, - #[cfg(debug_assertions)] - status: ::kas::WidgetStatus, - #stor_ty - } - - impl #impl_generics ::kas::layout::LayoutVisitor for #impl_target { - fn layout_visitor(&mut self) -> ::kas::layout::Visitor { - use ::kas::layout; - #layout_visitor - } - } - - impl #impl_generics ::kas::Layout for #impl_target { - fn size_rules( - &mut self, - sizer: ::kas::theme::SizeCx, - axis: ::kas::layout::AxisInfo, - ) -> ::kas::layout::SizeRules { - #[cfg(debug_assertions)] - #core_path.status.size_rules(&#core_path._id, axis); - ::kas::layout::LayoutVisitor::layout_visitor(self).size_rules(sizer, axis) - } - - fn set_rect( - &mut self, - cx: &mut ::kas::event::ConfigCx, - rect: ::kas::geom::Rect, - hints: ::kas::layout::AlignHints, - ) { - #[cfg(debug_assertions)] - #core_path.status.set_rect(&#core_path._id); - - #core_path._rect = rect; - ::kas::layout::LayoutVisitor::layout_visitor(self).set_rect(cx, rect, hints); - } - - fn try_probe(&mut self, coord: ::kas::geom::Coord) -> Option<::kas::Id> { - ::kas::Tile::rect(self).contains(coord).then(|| ::kas::Tile::probe(self, coord)) - } - - fn draw(&mut self, mut draw: ::kas::theme::DrawCx) { - #[cfg(debug_assertions)] - #core_path.status.require_rect(&#core_path._id); - - draw.set_id(::kas::Tile::id(self)); - - ::kas::layout::LayoutVisitor::layout_visitor(self).draw(draw); - } - } - - impl #impl_generics ::kas::Tile for #impl_target { - #core_impl - #num_children - fn get_child(&self, index: usize) -> Option<&dyn ::kas::Tile> { - match index { - #get_rules - _ => None, - } - } - - fn nav_next(&self, reverse: bool, from: Option) -> Option { - #nav_next - } - - #[inline] - fn probe(&mut self, coord: ::kas::geom::Coord) -> ::kas::Id { - #[cfg(debug_assertions)] - #core_path.status.require_rect(&#core_path._id); - - let coord = coord + ::kas::Tile::translation(self); - ::kas::layout::LayoutVisitor::layout_visitor(self) - .try_probe(coord) - .unwrap_or_else(|| ::kas::Tile::id(self)) - } - } - - impl #impl_generics ::kas::Events for #impl_target { - fn handle_event( - &mut self, - _: &mut ::kas::event::EventCx, - _: &Self::Data, - _: ::kas::event::Event, - ) -> ::kas::event::IsUsed { - #[cfg(debug_assertions)] - #core_path.status.require_rect(&#core_path._id); - ::kas::event::Unused - } - } - - #widget_impl - - #name { - _rect: Default::default(), - _id: Default::default(), - #[cfg(debug_assertions)] - status: Default::default(), - #stor_def - } - }}; - // println!("{}", toks); - Ok(toks) - } - - /// Parse a column (contents only) - pub fn column(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let stor = core_gen.next(); - let list = parse_layout_items(inner, &mut core_gen, false)?; - Ok(Tree(Layout::List(stor.into(), Direction::Down, list))) - } - - /// Parse a row (contents only) - pub fn row(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let stor = core_gen.next(); - let list = parse_layout_items(inner, &mut core_gen, false)?; - Ok(Tree(Layout::List(stor.into(), Direction::Right, list))) - } - - /// Parse an aligned column (contents only) - pub fn aligned_column(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let stor = core_gen.next(); - Ok(Tree(parse_grid_as_list_of_lists::( - stor.into(), - inner, - &mut core_gen, - true, - false, - )?)) - } - - /// Parse an aligned row (contents only) - pub fn aligned_row(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let stor = core_gen.next(); - Ok(Tree(parse_grid_as_list_of_lists::( - stor.into(), - inner, - &mut core_gen, - false, - false, - )?)) - } - - /// Parse direction, list - pub fn list(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let stor = core_gen.next(); - let dir: Direction = inner.parse()?; - let _: Token![,] = inner.parse()?; - let list = parse_layout_list(inner, &mut core_gen, false)?; - Ok(Tree(Layout::List(stor.into(), dir, list))) - } - - /// Parse a float (contents only) - pub fn float(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let list = parse_layout_items(inner, &mut core_gen, false)?; - Ok(Tree(Layout::Float(list))) - } - - /// Parse a grid (contents only) - pub fn grid(inner: ParseStream) -> Result { - let mut core_gen = NameGenerator::default(); - let stor = core_gen.next(); - Ok(Tree(parse_grid(stor.into(), inner, &mut core_gen, false)?)) - } } #[derive(Debug)] @@ -353,13 +133,11 @@ enum Layout { Pack(Box, Pack), Single(ExprMember), Widget(Ident, Expr), - Frame(StorIdent, Box, Expr), - Button(StorIdent, Box, Expr), - List(StorIdent, Direction, LayoutList<()>), + Frame(Ident, Box, Expr, Expr), + List(Ident, Direction, LayoutList<()>), Float(LayoutList<()>), - Grid(StorIdent, GridDimensions, LayoutList), + Grid(Ident, GridDimensions, LayoutList), Label(Ident, LitStr), - NonNavigable(Box), MapAny(Box, MapAny), } @@ -393,45 +171,13 @@ bitflags::bitflags! { impl Parse for Tree { fn parse(input: ParseStream) -> Result { let mut core_gen = NameGenerator::default(); - Ok(Tree(Layout::parse(input, &mut core_gen, true)?)) + Ok(Tree(Layout::parse(input, &mut core_gen)?)) } } impl Layout { - fn parse(input: ParseStream, core_gen: &mut NameGenerator, _recurse: bool) -> Result { - #[cfg(feature = "recursive-layout-widgets")] - let _recurse = true; - - #[cfg(not(feature = "recursive-layout-widgets"))] - if input.peek2(Token![!]) { - let input2 = input.fork(); - let mut temp_gen = NameGenerator::default(); - if Self::parse_macro_like(&input2, &mut temp_gen).is_ok() { - loop { - if let Ok(dot_token) = input2.parse::() { - if input2.peek(kw::map_any) { - let _ = MapAny::parse(dot_token, &input2)?; - continue; - } else if input2.peek(kw::align) { - let _ = Align::parse(dot_token, &input2)?; - continue; - } else if input2.peek(kw::pack) { - let _ = Pack::parse(dot_token, &input2, &mut temp_gen)?; - continue; - } else if let Ok(ident) = input2.parse::() { - proc_macro_error2::emit_warning!( - ident, "this method call is incompatible with feature `recursive-layout-widgets`"; - note = "extract operand from layout expression or wrap with braces", - ); - } - } - - break; - } - } - } - - let mut layout = if _recurse && input.peek2(Token![!]) { + fn parse(input: ParseStream, core_gen: &mut NameGenerator) -> Result { + let mut layout = if input.peek2(Token![!]) { Self::parse_macro_like(input, core_gen)? } else if input.peek(Token![self]) { Layout::Single(input.parse()?) @@ -456,19 +202,28 @@ impl Layout { let pack = Pack::parse(dot_token, input, core_gen)?; layout = Layout::Pack(Box::new(layout), pack); } else if let Ok(ident) = input.parse::() { + let note_msg = if matches!(&layout, &Layout::Frame(_, _, _, _)) { + "supported methods on layout objects: `map_any`, `align`, `pack`, `with_style`, `with_background`" + } else { + "supported methods on layout objects: `map_any`, `align`, `pack`" + }; emit_error!( ident, "method not supported here"; - note = "supported methods on layout objects: `map_any`, `align`, `pack`", + note = note_msg, ); // Clear remainder of input stream to avoid a redundant error - input.step(|cursor| { - let mut rest = *cursor; - while let Some((_, next)) = rest.token_tree() { - rest = next; - } - Ok(((), rest)) - })?; + let turbofish = if input.peek(Token![::]) { + Some(syn::AngleBracketedGenericArguments::parse_turbofish(input)?) + } else { + None + }; + + if turbofish.is_some() || input.peek(syn::token::Paren) { + let inner; + let _ = parenthesized!(inner in input); + let _ = inner.parse_terminated(Expr::parse, Token![,])?; + } // Continue with macro expansion to minimise secondary errors return Ok(layout); @@ -489,104 +244,95 @@ impl Layout { if lookahead.peek(kw::frame) { let _: kw::frame = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; + let stor = core_gen.next(); let inner; let _ = parenthesized!(inner in input); - let layout = Layout::parse(&inner, core_gen, true)?; - - let style: Expr = if !inner.is_empty() { - let _: Token![,] = inner.parse()?; - let _: kw::style = inner.parse()?; - let _: Token![=] = inner.parse()?; - inner.parse()? - } else { - syn::parse_quote! { ::kas::theme::FrameStyle::Frame } - }; - - Ok(Layout::Frame(stor, Box::new(layout), style)) - } else if lookahead.peek(kw::button) { - let _: kw::button = input.parse()?; - let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; - - let inner; - let _ = parenthesized!(inner in input); - let layout = Layout::parse(&inner, core_gen, true)?; + let layout = Layout::parse(&inner, core_gen)?; + + let mut style = None; + let mut bg = None; + while input.peek(Token![.]) { + if style.is_none() && input.peek2(kw::with_style) { + let _: Token![.] = input.parse()?; + let _: kw::with_style = input.parse()?; + + let inner; + let _ = parenthesized!(inner in input); + style = Some(inner.parse()?); + } else if bg.is_none() && input.peek2(kw::with_background) { + let _: Token![.] = input.parse()?; + let _: kw::with_background = input.parse()?; + + let inner; + let _ = parenthesized!(inner in input); + bg = Some(inner.parse()?); + } else { + break; + } + } - let color: Expr = if !inner.is_empty() { - let _: Token![,] = inner.parse()?; - let _: kw::color = inner.parse()?; - let _: Token![=] = inner.parse()?; - inner.parse()? - } else { - syn::parse_quote! { None } - }; + let style = + style.unwrap_or_else(|| syn::parse_quote! { ::kas::theme::FrameStyle::Frame }); + let bg = bg.unwrap_or_else(|| syn::parse_quote! { ::kas::theme::Background::Default }); - Ok(Layout::Button(stor, Box::new(layout), color)) + Ok(Layout::Frame(stor, Box::new(layout), style, bg)) } else if lookahead.peek(kw::column) { let _: kw::column = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; - let list = parse_layout_list(input, core_gen, true)?; + let stor = core_gen.next(); + let list = parse_layout_list(input, core_gen)?; Ok(Layout::List(stor, Direction::Down, list)) } else if lookahead.peek(kw::row) { let _: kw::row = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; - let list = parse_layout_list(input, core_gen, true)?; + let stor = core_gen.next(); + let list = parse_layout_list(input, core_gen)?; Ok(Layout::List(stor, Direction::Right, list)) } else if lookahead.peek(kw::list) { let _: kw::list = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; - let inner; - let _ = parenthesized!(inner in input); - let dir: Direction = inner.parse()?; - let _: Token![,] = inner.parse()?; - let list = parse_layout_list(&inner, core_gen, true)?; + let stor = core_gen.next(); + let list = parse_layout_list(input, core_gen)?; + let _: Token![.] = input.parse()?; + let _: kw::with_direction = input.parse()?; + let args; + let _ = parenthesized!(args in input); + let dir: Direction = args.parse()?; Ok(Layout::List(stor, dir, list)) } else if lookahead.peek(kw::float) { let _: kw::float = input.parse()?; let _: Token![!] = input.parse()?; - let list = parse_layout_list(input, core_gen, true)?; + let list = parse_layout_list(input, core_gen)?; Ok(Layout::Float(list)) } else if lookahead.peek(kw::aligned_column) { let _: kw::aligned_column = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; + let stor = core_gen.next(); let inner; let _ = bracketed!(inner in input); Ok(parse_grid_as_list_of_lists::( - stor, &inner, core_gen, true, true, + stor, &inner, core_gen, true, )?) } else if lookahead.peek(kw::aligned_row) { let _: kw::aligned_row = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; + let stor = core_gen.next(); let inner; let _ = bracketed!(inner in input); Ok(parse_grid_as_list_of_lists::( - stor, &inner, core_gen, false, true, + stor, &inner, core_gen, false, )?) } else if lookahead.peek(kw::grid) { let _: kw::grid = input.parse()?; let _: Token![!] = input.parse()?; - let stor = core_gen.parse_or_next(input)?; + let stor = core_gen.next(); let inner; let _ = braced!(inner in input); - Ok(parse_grid(stor, &inner, core_gen, true)?) - } else if lookahead.peek(kw::non_navigable) { - let _: kw::non_navigable = input.parse()?; - let _: Token![!] = input.parse()?; - - let inner; - let _ = parenthesized!(inner in input); - let layout = Layout::parse(&inner, core_gen, true)?; - Ok(Layout::NonNavigable(Box::new(layout))) + Ok(parse_grid(stor, &inner, core_gen)?) } else { let ident = core_gen.next(); let expr = input.parse()?; @@ -595,28 +341,20 @@ impl Layout { } } -fn parse_layout_list( - input: ParseStream, - core_gen: &mut NameGenerator, - recurse: bool, -) -> Result> { +fn parse_layout_list(input: ParseStream, core_gen: &mut NameGenerator) -> Result> { let inner; let _ = bracketed!(inner in input); - parse_layout_items(&inner, core_gen, recurse) + parse_layout_items(&inner, core_gen) } -fn parse_layout_items( - inner: ParseStream, - core_gen: &mut NameGenerator, - recurse: bool, -) -> Result> { +fn parse_layout_items(inner: ParseStream, core_gen: &mut NameGenerator) -> Result> { let mut list = vec![]; let mut gen2 = NameGenerator::default(); while !inner.is_empty() { list.push(ListItem { cell: (), stor: gen2.next(), - layout: Layout::parse(inner, core_gen, recurse)?, + layout: Layout::parse(inner, core_gen)?, }); if inner.is_empty() { @@ -630,11 +368,10 @@ fn parse_layout_items( } fn parse_grid_as_list_of_lists( - stor: StorIdent, + stor: Ident, inner: ParseStream, core_gen: &mut NameGenerator, row_major: bool, - recurse: bool, ) -> Result { let (mut col, mut row) = (0, 0); let mut dim = GridDimensions::default(); @@ -651,7 +388,7 @@ fn parse_grid_as_list_of_lists( while !inner2.is_empty() { let cell = CellInfo::new(col, row); dim.update(&cell); - let layout = Layout::parse(&inner2, core_gen, recurse)?; + let layout = Layout::parse(&inner2, core_gen)?; cells.push(ListItem { cell, stor: gen2.next(), @@ -687,12 +424,7 @@ fn parse_grid_as_list_of_lists( Ok(Layout::Grid(stor, dim, LayoutList(cells))) } -fn parse_grid( - stor: StorIdent, - inner: ParseStream, - core_gen: &mut NameGenerator, - recurse: bool, -) -> Result { +fn parse_grid(stor: Ident, inner: ParseStream, core_gen: &mut NameGenerator) -> Result { let mut dim = GridDimensions::default(); let mut gen2 = NameGenerator::default(); let mut cells = vec![]; @@ -706,10 +438,10 @@ fn parse_grid( if inner.peek(syn::token::Brace) { let inner2; let _ = braced!(inner2 in inner); - layout = Layout::parse(&inner2, core_gen, recurse)?; + layout = Layout::parse(&inner2, core_gen)?; require_comma = false; } else { - layout = Layout::parse(inner, core_gen, recurse)?; + layout = Layout::parse(inner, core_gen)?; require_comma = true; } cells.push(ListItem { @@ -848,7 +580,7 @@ struct Pack { pub kw: kw::pack, pub paren_token: token::Paren, pub hints: Expr, - pub stor: StorIdent, + pub stor: Ident, } impl Pack { fn parse( @@ -864,7 +596,7 @@ impl Pack { kw, paren_token, hints: content.parse()?, - stor: core_gen.next().into(), + stor: core_gen.next(), }) } @@ -882,7 +614,7 @@ impl Pack { impl Layout { fn append_fields(&self, fields: &mut StorageFields, children: &mut Vec, data_ty: &Type) { match self { - Layout::Align(layout, _) | Layout::NonNavigable(layout) => { + Layout::Align(layout, _) => { layout.append_fields(fields, children, data_ty); } Layout::Single(_) => (), @@ -907,7 +639,7 @@ impl Layout { .append_all(quote_spanned! {span=> #ident: Box::new(#expr), }); fields.used_data_ty = true; } - Layout::Frame(stor, layout, _) | Layout::Button(stor, layout, _) => { + Layout::Frame(stor, layout, _, _) => { fields .ty_toks .append_all(quote! { #stor: ::kas::layout::FrameStorage, }); @@ -1008,16 +740,10 @@ impl Layout { Layout::Widget(ident, _) => quote! { layout::Visitor::single(&mut #core_path.#ident) }, - Layout::Frame(stor, layout, style) => { + Layout::Frame(stor, layout, style, bg) => { let inner = layout.generate(core_path)?; quote! { - layout::Visitor::frame(&mut #core_path.#stor, #inner, #style) - } - } - Layout::Button(stor, layout, color) => { - let inner = layout.generate(core_path)?; - quote! { - layout::Visitor::button(&mut #core_path.#stor, #inner, #color) + layout::Visitor::frame(&mut #core_path.#stor, #inner, #style, #bg) } } Layout::List(stor, dir, list) => { @@ -1038,16 +764,13 @@ impl Layout { Layout::Label(stor, _) => { quote! { layout::Visitor::single(&mut #core_path.#stor) } } - Layout::NonNavigable(layout) | Layout::MapAny(layout, _) => { - return layout.generate(core_path) - } + Layout::MapAny(layout, _) => return layout.generate(core_path), }) } /// Create a Vec enumerating all children in navigation order /// /// - `output`: the result - /// - `index`: the next widget's index fn nav_next<'a, I: Clone + Iterator>( &self, children: I, @@ -1056,12 +779,8 @@ impl Layout { match self { Layout::Align(layout, _) | Layout::Pack(layout, _) - | Layout::Frame(_, layout, _) + | Layout::Frame(_, layout, _, _) | Layout::MapAny(layout, _) => layout.nav_next(children, output), - Layout::Button(_, _, _) | Layout::NonNavigable(_) => { - // Internals of a button are not navigable - Ok(()) - } Layout::Single(m) => { for (i, child) in children.enumerate() { if let ChildIdent::Field(ref ident) = child.ident { diff --git a/crates/kas-macros/src/widget_index.rs b/crates/kas-macros/src/visitors.rs similarity index 75% rename from crates/kas-macros/src/widget_index.rs rename to crates/kas-macros/src/visitors.rs index e80e6a21b..16be61c26 100644 --- a/crates/kas-macros/src/widget_index.rs +++ b/crates/kas-macros/src/visitors.rs @@ -53,11 +53,10 @@ impl Parse for WidgetInput { } } -struct Visitor<'a, I: Clone + Iterator> { +struct WidgetIndexVisitor<'a, I: Clone + Iterator> { children: I, - path_rect: TokenStream, } -impl<'a, I: Clone + Iterator> VisitMut for Visitor<'a, I> { +impl<'a, I: Clone + Iterator> VisitMut for WidgetIndexVisitor<'a, I> { fn visit_macro_mut(&mut self, node: &mut syn::Macro) { // HACK: we cannot expand the macro here since we do not have an Expr // to replace. Instead we can only modify the macro's tokens. @@ -84,7 +83,39 @@ impl<'a, I: Clone + Iterator> VisitMut for Visitor<' emit_error!(args.ident.span(), "does not match any child widget"); node.tokens = parse_quote! { error_emitted 0 }; return; - } else if node.path == parse_quote! { widget_set_rect } { + } + + visit_mut::visit_macro_mut(self, node); + } +} + +pub fn widget_index<'a, I: Clone + Iterator>( + children: I, + impls: &mut [syn::ItemImpl], +) { + let mut obj = WidgetIndexVisitor { children }; + + for impl_ in impls { + obj.visit_item_impl_mut(impl_); + } +} + +struct SetRectVisitor { + path_rect: TokenStream, + first_usage: Option, +} +impl VisitMut for SetRectVisitor { + fn visit_macro_mut(&mut self, node: &mut syn::Macro) { + // HACK: we cannot expand the macro here since we do not have an Expr + // to replace. Instead we can only modify the macro's tokens. + // WARNING: if the macro's tokens are modified before printing an error + // message is emitted then the span of that error message is incorrect. + + if node.path == parse_quote! { widget_set_rect } { + if self.first_usage.is_none() { + self.first_usage = Some(node.span()); + } + let expr = match syn::parse2::(node.tokens.clone()) { Ok(expr) => expr, Err(err) => { @@ -103,17 +134,14 @@ impl<'a, I: Clone + Iterator> VisitMut for Visitor<' } } -pub fn visit_impls<'a, I: Clone + Iterator>( - children: I, - path_rect: TokenStream, - impls: &mut [syn::ItemImpl], -) { - let mut obj = Visitor { - children, +/// Returns first span of widget_set_rect usage, if any +pub fn widget_set_rect(path_rect: TokenStream, block: &mut syn::Block) -> Option { + let mut obj = SetRectVisitor { path_rect, + first_usage: None, }; - for impl_ in impls { - obj.visit_item_impl_mut(impl_); - } + obj.visit_block_mut(block); + + obj.first_usage } diff --git a/crates/kas-macros/src/widget.rs b/crates/kas-macros/src/widget.rs index 7d509930b..b88fe1517 100644 --- a/crates/kas-macros/src/widget.rs +++ b/crates/kas-macros/src/widget.rs @@ -287,8 +287,7 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul ChildIdent::Field(ref member) => Some((i, member)), ChildIdent::CoreField(_) => None, }); - let path_rect = quote! { #core_path._rect }; - crate::widget_index::visit_impls(named_child_iter, path_rect, &mut scope.impls); + crate::visitors::widget_index(named_child_iter, &mut scope.impls); if let Some(ref span) = num_children { if get_child.is_none() { @@ -322,17 +321,15 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul #core_path.status.require_rect(&#core_path._id); }; - let mut required_tile_methods = impl_core_methods(&name.to_string(), &core_path); - let do_impl_widget_children = get_child.is_none() && for_child_node.is_none(); - if do_impl_widget_children { + let fns_get_child = if do_impl_widget_children { let mut get_rules = quote! {}; for (index, child) in children.iter().enumerate() { get_rules.append_all(child.ident.get_rule(&core_path, index)); } let count = children.len(); - required_tile_methods.append_all(quote! { + Some(quote! { fn num_children(&self) -> usize { #count } @@ -342,8 +339,10 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul _ => None, } } - }); - } + }) + } else { + None + }; if let Some(index) = widget_impl { let widget_impl = &mut scope.impls[index]; @@ -383,13 +382,7 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul }; let mut fn_draw = None; if let Some(Layout { tree, .. }) = args.layout.take() { - fn_nav_next = tree.nav_next(children.iter()).map(|toks| { - quote! { - fn nav_next(&self, reverse: bool, from: Option) -> Option { - #toks - } - } - }); + fn_nav_next = tree.nav_next(children.iter()); let layout_visitor = tree.layout_visitor(&core_path)?; scope.generated.push(quote! { @@ -548,6 +541,9 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul } }; + let core_rect_is_set; + let mut widget_set_rect_span = None; + let mut fn_set_rect_span = None; if let Some(index) = layout_impl { let layout_impl = &mut scope.impls[index]; let item_idents = collect_idents(layout_impl); @@ -573,16 +569,25 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul } if let Some((index, _)) = item_idents.iter().find(|(_, ident)| *ident == "set_rect") { - if let Some(ref core) = core_data { - if let ImplItem::Fn(f) = &mut layout_impl.items[*index] { + if let ImplItem::Fn(f) = &mut layout_impl.items[*index] { + fn_set_rect_span = Some(f.span()); + + let path_rect = quote! { #core_path._rect }; + widget_set_rect_span = crate::visitors::widget_set_rect(path_rect, &mut f.block); + core_rect_is_set = widget_set_rect_span.is_some(); + + if let Some(ref core) = core_data { f.block.stmts.insert(0, parse_quote! { #[cfg(debug_assertions)] self.#core.status.set_rect(&self.#core._id); }); } + } else { + core_rect_is_set = false; } } else { layout_impl.items.push(Verbatim(fn_set_rect)); + core_rect_is_set = true; } if let Some((index, _)) = item_idents.iter().find(|(_, ident)| *ident == "try_probe") { @@ -631,8 +636,19 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul #fn_draw } }); + core_rect_is_set = true; + } else { + core_rect_is_set = false; } + let mut have_fn_rect = core_rect_is_set; + let required_tile_methods = required_tile_methods(&name.to_string(), &core_path); + let fn_rect = if core_rect_is_set { + Some(fn_rect(&core_path)) + } else { + None + }; + if let Some(index) = tile_impl { let tile_impl = &mut scope.impls[index]; let item_idents = collect_idents(tile_impl); @@ -640,6 +656,37 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul tile_impl.items.push(Verbatim(required_tile_methods)); + if let Some((index, _)) = item_idents.iter().find(|(_, ident)| *ident == "rect") { + have_fn_rect = true; + if let Some(span) = widget_set_rect_span { + let fn_rect_span = tile_impl.items[*index].span(); + emit_warning!( + span, "assignment `widget_set_rect!` has no effect when `fn rect` is defined"; + note = fn_rect_span => "this `fn rect`"; + ); + } + if core_rect_is_set { + let fn_rect_span = tile_impl.items[*index].span(); + if let Some(span) = widget_set_rect_span { + emit_warning!( + span, "assignment `widget_set_rect!` has no effect when `fn rect` is defined"; + note = fn_rect_span => "this `fn rect`"; + ); + } else { + emit_warning!( + fn_rect_span, + "definition of `Layout::set_rect` is expected when `fn rect` is defined" + ); + } + } + } else if let Some(method) = fn_rect { + tile_impl.items.push(Verbatim(method)); + } + + if let Some(methods) = fns_get_child { + tile_impl.items.push(Verbatim(methods)); + } + if !has_item("nav_next") { match fn_nav_next { Ok(method) => tile_impl.items.push(Verbatim(method)), @@ -665,12 +712,25 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul scope.generated.push(quote! { impl #impl_generics ::kas::Tile for #impl_target { #required_tile_methods + #fn_rect + #fns_get_child #fn_nav_next #fn_probe } }); } + // TODO(opt): omit field widget.core._rect if !core_rect_is_set + + if !have_fn_rect { + if let Some(span) = fn_set_rect_span { + emit_warning!( + span, "`widget_set_rect!(/* rect */)` not found"; + note = "`fn Tile::rect()` implementation cannot be generated without `widget_set_rect!`"; + ); + } + } + if let Ok(val) = std::env::var("KAS_DEBUG_WIDGET") { if name == val.as_str() { println!("{}", scope.to_token_stream()); @@ -692,7 +752,7 @@ pub fn collect_idents(item_impl: &ItemImpl) -> Vec<(usize, Ident)> { .collect() } -pub fn impl_core_methods(name: &str, core_path: &Toks) -> Toks { +pub fn required_tile_methods(name: &str, core_path: &Toks) -> Toks { quote! { #[inline] fn as_tile(&self) -> &dyn ::kas::Tile { @@ -702,10 +762,6 @@ pub fn impl_core_methods(name: &str, core_path: &Toks) -> Toks { fn id_ref(&self) -> &::kas::Id { &#core_path._id } - #[inline] - fn rect(&self) -> ::kas::geom::Rect { - #core_path._rect - } #[inline] fn widget_name(&self) -> &'static str { @@ -714,6 +770,15 @@ pub fn impl_core_methods(name: &str, core_path: &Toks) -> Toks { } } +pub fn fn_rect(core_path: &Toks) -> Toks { + quote! { + #[inline] + fn rect(&self) -> ::kas::geom::Rect { + #core_path._rect + } + } +} + pub fn impl_widget( impl_generics: &Toks, impl_target: &Toks, diff --git a/crates/kas-widgets/src/adapt/adapt.rs b/crates/kas-widgets/src/adapt/adapt.rs index 12008a468..6022ece4a 100644 --- a/crates/kas-widgets/src/adapt/adapt.rs +++ b/crates/kas-widgets/src/adapt/adapt.rs @@ -17,25 +17,44 @@ impl_scope! { /// Where [`Map`] allows mapping to a sub-set of input data, `Adapt` allows /// mapping to a super-set (including internal storage). Further, `Adapt` /// supports message handlers which mutate internal storage. + /// + /// # Inner data type + /// + /// Note that, at least for now, the type of state stored by `Adapt` must + /// equal the data type of the inner widget: `state: ::Data`. + /// Since `W::Data` must outlive `W` (for our purposes this is not much + /// different than if `Widget::Data: 'static`), we cannot support `W::Data` + /// like `(&A, &S)` where `state: S`, so we might as well simply pass `&S` + /// to the inner widget `W`. This implies that any state from `A` which + /// needs to be passed into `W` must be *copied* into `state: W::Data` by + /// [`Adapt::on_update`]. + /// + /// (It is possible that the above restrictions will change in the future, + /// but they would require Rust to support generic associated types in + /// dyn-safe traits (also known as object safe GATs), at least for lifetime + /// parameters. There *was* an unstable feature for this, + /// `generic_associated_types_extended`, but it was removed due to being + /// stale, experimental and unsound. But even if Rust did gain this feature, + /// it is not clear that [`Widget::Data`] should be generic.) #[autoimpl(Scrollable using self.inner where W: trait)] #[widget { layout = self.inner; }] - pub struct Adapt, S: Debug> { + pub struct Adapt { core: widget_core!(), - state: S, + state: W::Data, #[widget(&self.state)] inner: W, - configure_handler: Option>, - update_handler: Option>, - timer_handlers: LinearMap>, - message_handlers: Vec>, + configure_handler: Option>, + update_handler: Option>, + timer_handlers: LinearMap>, + message_handlers: Vec>, } impl Self { /// Construct over `inner` with additional `state` #[inline] - pub fn new(inner: W, state: S) -> Self { + pub fn new(inner: W, state: W::Data) -> Self { Adapt { core: Default::default(), state, @@ -50,7 +69,7 @@ impl_scope! { /// Add a handler to be called on configuration pub fn on_configure(mut self, handler: F) -> Self where - F: Fn(&mut AdaptConfigCx, &mut S) + 'static, + F: Fn(&mut AdaptConfigCx, &mut W::Data) + 'static, { debug_assert!(self.configure_handler.is_none()); self.configure_handler = Some(Box::new(handler)); @@ -62,7 +81,7 @@ impl_scope! { /// Children will be updated after the handler is called. pub fn on_update(mut self, handler: F) -> Self where - F: Fn(&mut AdaptConfigCx, &mut S, &A) + 'static, + F: Fn(&mut AdaptConfigCx, &mut W::Data, &A) + 'static, { debug_assert!(self.update_handler.is_none()); self.update_handler = Some(Box::new(handler)); @@ -76,7 +95,7 @@ impl_scope! { /// of [`EventState::push_async`](kas::event::EventState::push_async). pub fn on_timer(mut self, timer_id: u64, handler: H) -> Self where - H: Fn(&mut AdaptEventCx, &mut S, &A) + 'static, + H: Fn(&mut AdaptEventCx, &mut W::Data, &A) + 'static, { debug_assert!(self.timer_handlers.get(&timer_id).is_none()); self.timer_handlers.insert(timer_id, Box::new(handler)); @@ -92,7 +111,7 @@ impl_scope! { pub fn on_message(self, handler: H) -> Self where M: Debug + 'static, - H: Fn(&mut AdaptEventCx, &mut S, M) + 'static, + H: Fn(&mut AdaptEventCx, &mut W::Data, M) + 'static, { self.on_messages(move |cx, state, _data| { if let Some(m) = cx.try_pop() { @@ -104,7 +123,7 @@ impl_scope! { /// Add a generic message handler pub fn on_messages(mut self, handler: H) -> Self where - H: Fn(&mut AdaptEventCx, &mut S, &A) + 'static, + H: Fn(&mut AdaptEventCx, &mut W::Data, &A) + 'static, { self.message_handlers.push(Box::new(handler)); self diff --git a/crates/kas-widgets/src/adapt/adapt_widget.rs b/crates/kas-widgets/src/adapt/adapt_widget.rs index 313b7f0cd..fb5ea87e2 100644 --- a/crates/kas-widgets/src/adapt/adapt_widget.rs +++ b/crates/kas-widgets/src/adapt/adapt_widget.rs @@ -185,5 +185,12 @@ pub trait AdaptWidget: Widget + Sized { { WithLabel::new_dir(self, direction, label) } + + /// Construct an [`Adapt`] widget over input + #[inline] + #[must_use] + fn with_state(self, state: Self::Data) -> Adapt { + Adapt::new(self, state) + } } impl AdaptWidget for W {} diff --git a/crates/kas-widgets/src/adapt/align.rs b/crates/kas-widgets/src/adapt/align.rs deleted file mode 100644 index a089e4e6d..000000000 --- a/crates/kas-widgets/src/adapt/align.rs +++ /dev/null @@ -1,81 +0,0 @@ -// 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 - -//! Alignment - -use kas::prelude::*; - -impl_scope! { - /// Apply an alignment hint - /// - /// The inner widget chooses how to apply (or ignore) this hint. - /// - /// Usually, this type will be constructed through one of the methods on - /// [`AdaptWidget`](crate::adapt::AdaptWidget). - #[widget{ derive = self.inner; }] - pub struct Align { - pub inner: W, - /// Hints may be modified directly. - /// - /// Use [`Action::RESIZE`] to apply changes. - pub hints: AlignHints, - } - - impl Self { - /// Construct - #[inline] - pub fn new(inner: W, hints: AlignHints) -> Self { - Align { inner, hints } - } - } - - impl Layout for Self { - fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) { - self.inner.set_rect(cx, rect, self.hints.combine(hints)); - } - } -} - -impl_scope! { - /// Apply an alignment hint, squash and align the result - /// - /// The inner widget chooses how to apply (or ignore) this hint. - /// The widget is then prevented from stretching beyond its ideal size, - /// aligning within the available rect. - /// - /// Usually, this type will be constructed through one of the methods on - /// [`AdaptWidget`](crate::adapt::AdaptWidget). - #[widget{ derive = self.inner; }] - pub struct Pack { - pub inner: W, - /// Hints may be modified directly. - /// - /// Use [`Action::RESIZE`] to apply changes. - pub hints: AlignHints, - size: Size, - } - - impl Self { - /// Construct - #[inline] - pub fn new(inner: W, hints: AlignHints) -> Self { - Pack { inner, hints, size: Size::ZERO } - } - } - - impl Layout for Self { - fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules { - let rules = self.inner.size_rules(sizer, axis); - self.size.set_component(axis, rules.ideal_size()); - rules - } - - fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) { - let align = self.hints.combine(hints).complete_default(); - let rect = align.aligned_rect(self.size, rect); - self.inner.set_rect(cx, rect, hints); - } - } -} diff --git a/crates/kas-widgets/src/adapt/mod.rs b/crates/kas-widgets/src/adapt/mod.rs index 6c22abfd5..c2b084b0a 100644 --- a/crates/kas-widgets/src/adapt/mod.rs +++ b/crates/kas-widgets/src/adapt/mod.rs @@ -9,7 +9,6 @@ mod adapt; mod adapt_cx; mod adapt_events; mod adapt_widget; -mod align; mod reserve; mod with_label; @@ -17,7 +16,7 @@ pub use adapt::{Adapt, Map}; pub use adapt_cx::{AdaptConfigCx, AdaptEventCx}; pub use adapt_events::AdaptEvents; pub use adapt_widget::*; -pub use align::{Align, Pack}; #[doc(inline)] pub use kas::hidden::MapAny; +pub use kas::hidden::{Align, Pack}; pub use reserve::{Margins, Reserve}; pub use with_label::WithLabel; diff --git a/crates/kas-widgets/src/adapt/with_label.rs b/crates/kas-widgets/src/adapt/with_label.rs index da6b1c938..8cec44b2d 100644 --- a/crates/kas-widgets/src/adapt/with_label.rs +++ b/crates/kas-widgets/src/adapt/with_label.rs @@ -6,7 +6,7 @@ //! Wrapper adding a label use crate::AccessLabel; -use kas::{layout, prelude::*}; +use kas::prelude::*; impl_scope! { /// A wrapper widget with a label @@ -18,7 +18,7 @@ impl_scope! { #[derive(Clone, Default)] #[widget { Data = W::Data; - layout = list! 'row (self.dir, [self.inner, non_navigable!(self.label)]); + layout = list![self.inner, self.label].with_direction(self.dir); }] pub struct WithLabel { core: widget_core!(), @@ -72,15 +72,6 @@ impl_scope! { 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.core.row - } - /// Get whether line-wrapping is enabled #[inline] pub fn wrap(&self) -> bool { @@ -112,6 +103,10 @@ impl_scope! { } impl Tile for Self { + fn nav_next(&self, _: bool, from: Option) -> Option { + from.xor(Some(widget_index!(self.inner))) + } + fn probe(&mut self, _: Coord) -> Id { self.inner.id() } diff --git a/crates/kas-widgets/src/button.rs b/crates/kas-widgets/src/button.rs index 15d6118a1..51c4bd0a5 100644 --- a/crates/kas-widgets/src/button.rs +++ b/crates/kas-widgets/src/button.rs @@ -6,9 +6,9 @@ //! Push-buttons use super::AccessLabel; -use kas::draw::color::Rgb; use kas::event::Key; use kas::prelude::*; +use kas::theme::{Background, FrameStyle}; use std::fmt::Debug; impl_scope! { @@ -17,14 +17,17 @@ impl_scope! { /// Default alignment of content is centered. #[widget { Data = W::Data; - layout = button!(self.inner, color = self.color); + layout = frame!(self.inner) + .with_style(FrameStyle::Button) + .with_background(self.bg) + .align(AlignHints::CENTER); navigable = true; hover_highlight = true; }] pub struct Button { core: widget_core!(), key: Option, - color: Option, + bg: Background, #[widget] pub inner: W, on_press: Option>, @@ -37,7 +40,7 @@ impl_scope! { Button { core: Default::default(), key: Default::default(), - color: None, + bg: Background::Default, inner, on_press: None, } @@ -80,19 +83,23 @@ impl_scope! { self } - /// Set button color - pub fn set_color(&mut self, color: Option) { - self.color = color; - } - - /// Set button color (chain style) + /// Set the frame background color (inline) + /// + /// The default background is [`Background::Default`]. + #[inline] #[must_use] - pub fn with_color(mut self, color: Rgb) -> Self { - self.color = Some(color); + pub fn with_background(mut self, bg: Background) -> Self { + self.bg = bg; self } } + impl Tile for Self { + fn probe(&mut self, _: Coord) -> Id { + self.id() + } + } + impl Events for Self { fn configure(&mut self, cx: &mut ConfigCx) { if let Some(key) = self.key.clone() { diff --git a/crates/kas-widgets/src/check_box.rs b/crates/kas-widgets/src/check_box.rs index 57331d06f..25d0cad15 100644 --- a/crates/kas-widgets/src/check_box.rs +++ b/crates/kas-widgets/src/check_box.rs @@ -174,7 +174,7 @@ impl_scope! { /// /// This is a [`CheckBox`] with a label. #[widget{ - layout = list!(self.direction(), [self.inner, non_navigable!(self.label)]); + layout = list![self.inner, self.label].with_direction(self.direction()); }] pub struct CheckButton { core: widget_core!(), @@ -194,6 +194,10 @@ impl_scope! { } impl Tile for Self { + fn nav_next(&self, _: bool, from: Option) -> Option { + from.xor(Some(widget_index!(self.inner))) + } + fn probe(&mut self, _: Coord) -> Id { self.inner.id() } diff --git a/crates/kas-widgets/src/combobox.rs b/crates/kas-widgets/src/combobox.rs index 7caf1c7a7..b7a2f49d9 100644 --- a/crates/kas-widgets/src/combobox.rs +++ b/crates/kas-widgets/src/combobox.rs @@ -9,6 +9,7 @@ use crate::adapt::AdaptEvents; use crate::{menu::MenuEntry, Column, Label, Mark}; use kas::event::{Command, FocusSource, ScrollDelta}; use kas::prelude::*; +use kas::theme::FrameStyle; use kas::theme::{MarkStyle, TextClass}; use kas::Popup; use std::fmt::Debug; @@ -28,7 +29,9 @@ impl_scope! { /// when selected. If a handler is specified via [`Self::with`] or /// [`Self::with_msg`] then this message is passed to the handler and not emitted. #[widget { - layout = button! 'frame(row! [self.label, self.mark]); + layout = frame!(row! [self.label, self.mark]) + .with_style(FrameStyle::Button) + .align(AlignHints::CENTER); navigable = true; hover_highlight = true; }] @@ -51,6 +54,10 @@ impl_scope! { // We have no child within our rect None } + + fn probe(&mut self, _: Coord) -> Id { + self.id() + } } impl Events for Self { diff --git a/crates/kas-widgets/src/float.rs b/crates/kas-widgets/src/float.rs new file mode 100644 index 000000000..7bbd698bd --- /dev/null +++ b/crates/kas-widgets/src/float.rs @@ -0,0 +1,174 @@ +// 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 + +//! A pile of widgets "floating" over the top of each other + +use kas::prelude::*; +use kas::Collection; + +/// Make a [`Float`] widget +/// +/// # Syntax +/// +/// > _Collection_ :\ +/// >    `float!` `[` _Items_\? `]` +/// > +/// > _Items_ :\ +/// >    (_Item_ `,`)\* _Item_ `,`\? +/// +/// ## Stand-alone usage +/// +/// When used as a stand-alone macro, `float! [/* ... */]` is just syntactic sugar +/// for `Float::new(kas::collection! [/* ... */])`. +/// +/// In this case, _Item_ may be: +/// +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` +/// +/// In case all _Item_ instances are a string literal, the data type of the +/// `float!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. +/// +/// ## Usage within widget layout syntax +/// +/// When called within [widget layout syntax], `float!` may be evaluated as a +/// recursive macro and the result does not have a specified type, except that +/// methods [`map_any`], [`align`] and [`pack`] are supported via emulation. +/// +/// In this case, _Item_ is evaluated using [widget layout syntax]. This is +/// broadly similar to the above with a couple of exceptions: +/// +/// - Supported layout macros do not need to be imported to the module scope +/// - An _Item_ may be a `#[widget]` field of the widget +/// +/// # Example +/// +/// ``` +/// # use kas::prelude::*; +/// let my_widget = kas_widgets::float! [ +/// "one".pack(AlignHints::TOP_LEFT), +/// "two".pack(AlignHints::BOTTOM_RIGHT), +/// "some text\nin the\nbackground" +/// ]; +/// ``` +/// +/// [widget layout syntax]: macro@widget#layout-1 +/// [`map_any`]: crate::AdaptWidgetAny::map_any +/// [`align`]: crate::AdaptWidget::align +/// [`pack`]: crate::AdaptWidget::pack +#[macro_export] +macro_rules! float { + ( $( $ee:expr ),* ) => { + $crate::Float::new( ::kas::collection! [ $( $ee ),* ] ) + }; + ( $( $ee:expr ),+ , ) => { + $crate::Float::new( ::kas::collection! [ $( $ee ),+ ] ) + }; +} + +impl_scope! { + /// A float widget + /// + /// All widgets occupy the same space with the first child on top. + /// + /// Size is determined as the maximum required by any child for each axis. + /// All widgets are assigned this size. It is usually necessary to use + /// [`pack`] or a similar mechanism to constrain a child to avoid it hiding + /// the content underneath (note that even if an unconstrained child does + /// not *visually* hide everything beneath, it may still "occupy" the + /// assigned area, preventing mouse clicks from reaching the widget + /// beneath). + /// + /// [`pack`]: crate::AdaptWidget::pack + #[derive(Clone, Default)] + #[widget] + pub struct Float { + core: widget_core!(), + widgets: C, + } + + impl Self { + /// Construct a float + #[inline] + pub fn new(widgets: C) -> Self { + Float { + core: Default::default(), + widgets, + } + } + } + + impl Layout for Self { + fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules { + let mut rules = SizeRules::EMPTY; + for i in 0..self.widgets.len() { + if let Some(child) = self.widgets.get_mut_tile(i) { + rules = rules.max(child.size_rules(sizer.re(), axis)); + } + } + rules + } + + fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) { + widget_set_rect!(rect); + for i in 0..self.widgets.len() { + if let Some(child) = self.widgets.get_mut_tile(i) { + child.set_rect(cx, rect, hints); + } + } + } + + fn draw(&mut self, mut draw: DrawCx) { + let mut iter = (0..self.widgets.len()).rev(); + if let Some(first) = iter.next() { + if let Some(child) = self.widgets.get_mut_tile(first) { + child.draw(draw.re()); + } + } + for i in iter { + if let Some(child) = self.widgets.get_mut_tile(i) { + draw.with_pass(|draw| child.draw(draw)); + } + } + } + } + + impl Tile for Self { + #[inline] + fn num_children(&self) -> usize { + self.widgets.len() + } + + fn get_child(&self, index: usize) -> Option<&dyn Tile> { + self.widgets.get_tile(index) + } + + fn probe(&mut self, coord: Coord) -> Id { + for i in 0..self.widgets.len() { + if let Some(child) = self.widgets.get_mut_tile(i) { + if let Some(id) = child.try_probe(coord) { + return id; + } + } + } + self.id() + } + } + + impl Widget for Self { + type Data = C::Data; + + fn for_child_node( + &mut self, + data: &C::Data, + index: usize, + closure: Box) + '_>, + ) { + self.widgets.for_node(data, index, closure); + } + } +} diff --git a/crates/kas-widgets/src/frame.rs b/crates/kas-widgets/src/frame.rs index 44eb6a456..74ae5b236 100644 --- a/crates/kas-widgets/src/frame.rs +++ b/crates/kas-widgets/src/frame.rs @@ -6,6 +6,41 @@ //! A simple frame use kas::prelude::*; +use kas::theme::{Background, FrameStyle}; + +/// Make a [`Frame`] widget +/// +/// # Syntax +/// +/// ## Stand-alone usage +/// +/// When called as a stand-alone macro, `frame!(inner)` is just syntactic sugar +/// for `Frame::new(inner)`, and yes, this makes the macro pointless. +/// +/// ## Usage within widget layout syntax +/// +/// When called within [widget layout syntax], `frame!` may be evaluated as a +/// recursive macro and the result does not have a specified type, except that +/// methods [`map_any`], [`align`], [`pack`] and [`with_style`] are supported +/// via emulation. +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::frame!(kas_widgets::Label::new("content")); +/// ``` +/// +/// [widget layout syntax]: macro@widget#layout-1 +/// [`map_any`]: crate::AdaptWidgetAny::map_any +/// [`align`]: crate::AdaptWidget::align +/// [`pack`]: crate::AdaptWidget::pack +/// [`with_style`]: Frame::with_style +#[macro_export] +macro_rules! frame { + ( $e:expr ) => { + $crate::Frame::new($e) + }; +} impl_scope! { /// A frame around content @@ -18,10 +53,12 @@ impl_scope! { #[derive(Clone, Default)] #[widget{ Data = W::Data; - layout = frame!(self.inner); + layout = frame!(self.inner).with_style(self.style); }] pub struct Frame { core: widget_core!(), + style: FrameStyle, + bg: Background, /// The inner widget #[widget] pub inner: W, @@ -33,8 +70,33 @@ impl_scope! { pub fn new(inner: W) -> Self { Frame { core: Default::default(), + style: FrameStyle::Frame, + bg: Background::default(), inner, } } + + /// Set the frame style (inline) + /// + /// The default style is [`FrameStyle::Frame`]. + /// + /// Note: using [`FrameStyle::NavFocus`] does not automatically make + /// this widget interactive. Use [`NavFrame`](crate::NavFrame) for that. + #[inline] + #[must_use] + pub fn with_style(mut self, style: FrameStyle) -> Self { + self.style = style; + self + } + + /// Set the frame background color (inline) + /// + /// The default background is [`Background::Default`]. + #[inline] + #[must_use] + pub fn with_background(mut self, bg: Background) -> Self { + self.bg = bg; + self + } } } diff --git a/crates/kas-widgets/src/grid.rs b/crates/kas-widgets/src/grid.rs index 027a18084..54ce2a6da 100644 --- a/crates/kas-widgets/src/grid.rs +++ b/crates/kas-widgets/src/grid.rs @@ -10,6 +10,124 @@ use kas::layout::{GridSetter, GridSolver, RulesSetter, RulesSolver}; use kas::{layout, prelude::*, CellCollection}; use std::ops::{Index, IndexMut}; +/// Make a [`Grid`] widget +/// +/// Constructs a table with auto-determined number of rows and columns. +/// Cells may overlap, in which case behaviour is identical to [`float!`]: the +/// first declared item is on top. +/// +/// # Syntax +/// +/// > _Collection_ :\ +/// >    `collection!` `[` _ItemArms_\? `]` +/// > +/// > _ItemArms_ :\ +/// >    (_ItemArm_ `,`)\* _ItemArm_ `,`\? +/// > +/// > _ItemArm_ :\ +/// >    `(` _Column_ `,` _Row_ `)` `=>` _Item_ +/// > +/// > _Column_, _Row_ :\ +/// >    _LitInt_ | ( _LitInt_ `..` `+` _LitInt_ ) | ( _LitInt_ `..` +/// > _LitInt_ ) | ( _LitInt_ `..=` _LitInt_ ) +/// +/// Here, _Column_ and _Row_ are selected via an index (from 0), a range of +/// indices, or a start + increment. For example, `2` = `2..+1` = `2..3` = +/// `2..=2` while `5..+2` = `5..7` = `5..=6`. +/// +/// ## Stand-alone usage +/// +/// When used as a stand-alone macro, `grid! [/* ... */]` is just syntactic +/// sugar for `Grid::new(kas::cell_collection! [/* ... */])`. +/// +/// In this case, _Item_ may be: +/// +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` +/// +/// In case all _Item_ instances are a string literal, the data type of the +/// `grid!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. +/// +/// ## Usage within widget layout syntax +/// +/// In this case, _Item_ uses [widget layout syntax]. This is broadly similar to +/// the above with a couple of exceptions: +/// +/// - Supported layout macros do not need to be imported to the module scope +/// - An _Item_ may be a `#[widget]` field of the widget +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::grid! { +/// (0, 0) => "one", +/// (1, 0) => "two", +/// (0..2, 1) => "three", +/// }; +/// ``` +/// +/// [widget layout syntax]: macro@widget#layout-1 +/// [`align`]: crate::AdaptWidget::align +/// [`pack`]: crate::AdaptWidget::pack +/// [`float!`]: crate::float +#[macro_export] +macro_rules! grid { + ( $( ($cc:expr, $rr:expr) => $ee:expr ),* ) => { + $crate::Grid::new( ::kas::cell_collection! [ $( ($cc, $rr) => $ee ),* ] ) + }; + ( $( ($cc:expr, $rr:expr) => $ee:expr ),+ , ) => { + $crate::Grid::new( ::kas::cell_collection! [ $( ($cc, $rr) => $ee ),+ ] ) + }; +} + +/// Define a [`Grid`] as a sequence of rows +/// +/// This is just special convenience syntax for defining a [`Grid`]. See also +/// [`grid!`] documentation. +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::aligned_column! [ +/// row!["one", "two"], +/// row!["three", "four"], +/// ]; +/// ``` +#[macro_export] +macro_rules! aligned_column { + () => { + $crate::Grid::new(::kas::cell_collection! []) + }; + ($(row![$($ee:expr),* $(,)?]),+ $(,)?) => { + $crate::Grid::new(::kas::cell_collection![aligned_column $(row![$($ee),*]),+]) + }; +} + +/// Define a [`Grid`] as a sequence of columns +/// +/// This is just special convenience syntax for defining a [`Grid`]. See also +/// [`grid!`] documentation. +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::aligned_row! [ +/// column!["one", "two"], +/// column!["three", "four"], +/// ]; +/// ``` +#[macro_export] +macro_rules! aligned_row { + () => { + $crate::Grid::new(::kas::cell_collection! []) + }; + ($(column![$($ee:expr),* $(,)?]),+ $(,)?) => { + $crate::Grid::new(::kas::cell_collection![aligned_row $(column![$($ee),*]),+]) + }; +} + impl_scope! { /// A generic grid widget /// diff --git a/crates/kas-widgets/src/grip.rs b/crates/kas-widgets/src/grip.rs index 461361189..c74736e47 100644 --- a/crates/kas-widgets/src/grip.rs +++ b/crates/kas-widgets/src/grip.rs @@ -73,6 +73,8 @@ impl_scope! { core: widget_core!(), // The track is the area within which this GripPart may move track: Rect, + // The position of the grip handle + rect: Rect, press_coord: Coord, } @@ -82,9 +84,19 @@ impl_scope! { SizeRules::EMPTY } + fn set_rect(&mut self, _: &mut ConfigCx, rect: Rect, _: AlignHints) { + self.rect = rect; + } + fn draw(&mut self, _: DrawCx) {} } + impl Tile for Self { + fn rect(&self) -> Rect { + self.rect + } + } + impl Events for GripPart { type Data = (); @@ -121,6 +133,7 @@ impl_scope! { GripPart { core: Default::default(), track: Default::default(), + rect: Default::default(), press_coord: Coord::ZERO, } } @@ -153,7 +166,7 @@ impl_scope! { /// /// This size may be read via `self.rect().size`. pub fn set_size(&mut self, size: Size) { - widget_set_rect!(Rect { pos: self.rect().pos, size, }); + self.rect.size = size; } /// Get the current grip position @@ -187,7 +200,7 @@ impl_scope! { let offset = offset.min(self.max_offset()).max(Offset::ZERO); let grip_pos = self.track.pos + offset; if grip_pos != self.rect().pos { - widget_set_rect!(Rect { pos: grip_pos, size: self.rect().size }); + self.rect.pos = grip_pos; cx.redraw(self); } offset diff --git a/crates/kas-widgets/src/lib.rs b/crates/kas-widgets/src/lib.rs index 7d7b9899f..734626ba1 100644 --- a/crates/kas-widgets/src/lib.rs +++ b/crates/kas-widgets/src/lib.rs @@ -71,6 +71,7 @@ pub mod dialog; pub mod edit; mod event_config; mod filler; +mod float; mod frame; mod grid; mod grip; @@ -94,8 +95,6 @@ mod stack; mod tab_stack; mod text; -pub use kas_macros::{aligned_column, aligned_row, column, float, grid, list, row}; - pub use crate::image::Image; #[cfg(feature = "image")] pub use crate::image::ImageError; pub use button::Button; @@ -104,6 +103,7 @@ pub use combobox::ComboBox; pub use edit::{EditBox, EditField, EditGuard}; pub use event_config::EventConfig; pub use filler::Filler; +pub use float::Float; pub use frame::Frame; pub use grid::Grid; pub use grip::{GripMsg, GripPart}; diff --git a/crates/kas-widgets/src/list.rs b/crates/kas-widgets/src/list.rs index acbd25aab..b0e7ad3e0 100644 --- a/crates/kas-widgets/src/list.rs +++ b/crates/kas-widgets/src/list.rs @@ -14,6 +14,186 @@ use kas::Collection; use std::collections::hash_map::{Entry, HashMap}; use std::ops::{Index, IndexMut}; +/// Make a [`Row`] widget +/// +/// # Syntax +/// +/// > _Collection_ :\ +/// >    `row!` `[` _Items_\? `]` +/// > +/// > _Items_ :\ +/// >    (_Item_ `,`)\* _Item_ `,`\? +/// +/// ## Stand-alone usage +/// +/// When used as a stand-alone macro, `row! [/* ... */]` is just syntactic sugar +/// for `Row::new(kas::collection! [/* ... */])`. +/// +/// In this case, _Item_ may be: +/// +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` +/// +/// In case all _Item_ instances are a string literal, the data type of the +/// `row!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. +/// +/// ## Usage within widget layout syntax +/// +/// When called within [widget layout syntax], `row!` may be evaluated as a +/// recursive macro and the result does not have a specified type, except that +/// methods [`map_any`], [`align`] and [`pack`] are supported via emulation. +/// +/// In this case, _Item_ is evaluated using [widget layout syntax]. This is +/// broadly similar to the above with a couple of exceptions: +/// +/// - Supported layout macros do not need to be imported to the module scope +/// - An _Item_ may be a `#[widget]` field of the widget +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::row!["one", "two"]; +/// ``` +/// +/// [widget layout syntax]: macro@widget#layout-1 +/// [`map_any`]: crate::AdaptWidgetAny::map_any +/// [`align`]: crate::AdaptWidget::align +/// [`pack`]: crate::AdaptWidget::pack +#[macro_export] +macro_rules! row { + ( $( $ee:expr ),* ) => { + $crate::Row::new( ::kas::collection! [ $( $ee ),* ] ) + }; + ( $( $ee:expr ),+ , ) => { + $crate::Row::new( ::kas::collection! [ $( $ee ),+ ] ) + }; +} + +/// Make a [`Column`] widget +/// +/// # Syntax +/// +/// > _Collection_ :\ +/// >    `column!` `[` _Items_\? `]` +/// > +/// > _Items_ :\ +/// >    (_Item_ `,`)\* _Item_ `,`\? +/// +/// ## Stand-alone usage +/// +/// When used as a stand-alone macro, `column! [/* ... */]` is just syntactic sugar +/// for `Column::new(kas::collection! [/* ... */])`. +/// +/// In this case, _Item_ may be: +/// +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` +/// +/// In case all _Item_ instances are a string literal, the data type of the +/// `column!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. +/// +/// ## Usage within widget layout syntax +/// +/// When called within [widget layout syntax], `column!` may be evaluated as a +/// recursive macro and the result does not have a specified type, except that +/// methods [`map_any`], [`align`] and [`pack`] are supported via emulation. +/// +/// In this case, _Item_ is evaluated using [widget layout syntax]. This is +/// broadly similar to the above with a couple of exceptions: +/// +/// - Supported layout macros do not need to be imported to the module scope +/// - An _Item_ may be a `#[widget]` field of the widget +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::column! [ +/// "one", +/// "two", +/// ]; +/// ``` +/// +/// [widget layout syntax]: macro@widget#layout-1 +/// [`map_any`]: crate::AdaptWidgetAny::map_any +/// [`align`]: crate::AdaptWidget::align +/// [`pack`]: crate::AdaptWidget::pack +#[macro_export] +macro_rules! column { + ( $( $ee:expr ),* ) => { + $crate::Column::new( ::kas::collection! [ $( $ee ),* ] ) + }; + ( $( $ee:expr ),+ , ) => { + $crate::Column::new( ::kas::collection! [ $( $ee ),+ ] ) + }; +} + +/// Make a [`List`] widget +/// +/// # Syntax +/// +/// > _Collection_ :\ +/// >    `list!` `[` _Items_\? `]` +/// > +/// > _Items_ :\ +/// >    (_Item_ `,`)\* _Item_ `,`\? +/// +/// ## Stand-alone usage +/// +/// When used as a stand-alone macro, `list! [/* ... */]` is just syntactic sugar +/// for `List::new(kas::collection! [/* ... */])`. +/// +/// In this case, _Item_ may be: +/// +/// - A string literal (interpreted as a label widget), optionally followed by +/// an [`align`] or [`pack`] method call +/// - An expression yielding an object implementing `Widget` +/// +/// In case all _Item_ instances are a string literal, the data type of the +/// `list!` widget will be `()`; otherwise the data type of the widget is `_A` +/// where `_A` is a generic type parameter of the widget. +/// +/// ## Usage within widget layout syntax +/// +/// When called within [widget layout syntax], `list!` may be evaluated as a +/// recursive macro and the result does not have a specified type, except that +/// methods [`map_any`], [`align`], [`pack`] and [`with_direction`] are +/// supported via emulation. In this case, calling [`with_direction`] is +/// required. Note that the argument passed to [`with_direction`] is expanded +/// at the use site, so for example `.with_direction(self.dir)` will read +/// `self.dir` whenever layout is computed. +/// +/// In this case, _Item_ is evaluated using [widget layout syntax]. This is +/// broadly similar to the above with a couple of exceptions: +/// +/// - Supported layout macros do not need to be imported to the module scope +/// - An _Item_ may be a `#[widget]` field of the widget +/// +/// # Example +/// +/// ``` +/// let my_widget = kas_widgets::list! ["one", "two"] +/// .with_direction(kas::dir::Left); +/// ``` +/// +/// [widget layout syntax]: macro@widget#layout-1 +/// [`map_any`]: crate::AdaptWidgetAny::map_any +/// [`align`]: crate::AdaptWidget::align +/// [`pack`]: crate::AdaptWidget::pack +/// [`with_direction`]: List::with_direction +#[macro_export] +macro_rules! list { + ( $( $ee:expr ),* ) => { + $crate::List::new( ::kas::collection! [ $( $ee ),* ] ) + }; + ( $( $ee:expr ),+ , ) => { + $crate::List::new( ::kas::collection! [ $( $ee ),+ ] ) + }; +} + /// A generic row widget /// /// See documentation of [`List`] type. @@ -261,9 +441,9 @@ impl_scope! { } } - impl List { + impl List { /// Set the direction of contents - pub fn set_direction(&mut self, cx: &mut EventState, direction: Direction) { + pub fn set_direction(&mut self, cx: &mut EventState, direction: D) { if direction == self.direction { return; } @@ -293,6 +473,13 @@ impl_scope! { self.direction.as_direction() } + /// Set the direction of contents (inline) + #[inline] + pub fn with_direction(mut self, direction: D) -> Self { + self.direction = direction; + self + } + /// Access layout storage /// /// The number of columns/rows is [`Self.len`]. diff --git a/crates/kas-widgets/src/nav_frame.rs b/crates/kas-widgets/src/nav_frame.rs index f9833eb3f..2b063cffe 100644 --- a/crates/kas-widgets/src/nav_frame.rs +++ b/crates/kas-widgets/src/nav_frame.rs @@ -22,7 +22,7 @@ impl_scope! { #[widget{ Data = W::Data; navigable = true; - layout = frame!(self.inner, style = kas::theme::FrameStyle::NavFocus); + layout = frame!(self.inner).with_style(kas::theme::FrameStyle::NavFocus); }] pub struct NavFrame { core: widget_core!(), diff --git a/crates/kas-widgets/src/radio_box.rs b/crates/kas-widgets/src/radio_box.rs index 6f732fbde..124f82c95 100644 --- a/crates/kas-widgets/src/radio_box.rs +++ b/crates/kas-widgets/src/radio_box.rs @@ -135,7 +135,7 @@ impl_scope! { /// /// See also [`RadioBox`] which excludes the label. #[widget{ - layout = list!(self.direction(), [self.inner, non_navigable!(self.label)]); + layout = list![self.inner, self.label].with_direction(self.direction()); }] pub struct RadioButton { core: widget_core!(), @@ -155,6 +155,10 @@ impl_scope! { } impl Tile for Self { + fn nav_next(&self, _: bool, from: Option) -> Option { + from.xor(Some(widget_index!(self.inner))) + } + fn probe(&mut self, _: Coord) -> Id { self.inner.id() } diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index 65158c8f0..5e99cb982 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -163,10 +163,8 @@ impl_scope! { /// - With floating-point types, ensure that `step` is exactly /// representable, e.g. an integer or a power of 2. #[widget { - layout = frame!(row! [ - self.edit, - column! [self.b_up, self.b_down], - ], style = FrameStyle::EditBox); + layout = frame!(row![self.edit, column! [self.b_up, self.b_down]]) + .with_style(FrameStyle::EditBox); }] pub struct Spinner { core: widget_core!(), diff --git a/crates/kas-widgets/src/tab_stack.rs b/crates/kas-widgets/src/tab_stack.rs index 4be72db1c..b2e6027cc 100644 --- a/crates/kas-widgets/src/tab_stack.rs +++ b/crates/kas-widgets/src/tab_stack.rs @@ -21,7 +21,7 @@ impl_scope! { /// This is a special variant of `Button` which sends a [`Select`] on press. #[widget { Data = (); - layout = frame!(self.label, style = FrameStyle::Tab); + layout = frame!(self.label).with_style(FrameStyle::Tab); navigable = true; hover_highlight = true; }] @@ -99,10 +99,7 @@ impl_scope! { /// See also the main implementing widget: [`Stack`]. #[impl_default(Self::new())] #[widget { - layout = list!(self.direction, [ - self.stack, - self.tabs, - ]); + layout = list![self.stack, self.tabs].with_direction(self.direction); }] pub struct TabStack { core: widget_core!(), diff --git a/examples/counter.rs b/examples/counter.rs index bb9390af5..a9cc5024b 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -6,7 +6,7 @@ //! Counter example (simple button) use kas::prelude::*; -use kas::widgets::{column, format_value, row, Adapt, Button}; +use kas::widgets::{column, format_value, row, Button}; #[derive(Clone, Debug)] struct Increment(i32); @@ -21,7 +21,8 @@ fn counter() -> impl Widget { .map_any(), ]; - Adapt::new(tree, 0).on_message(|_, count, Increment(add)| *count += add) + tree.with_state(0) + .on_message(|_, count, Increment(add)| *count += add) } fn main() -> kas::runner::Result<()> { diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index 67f2247fc..41fd2e29b 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -212,7 +212,9 @@ fn main() -> kas::runner::Result<()> { ScrollBars::new(list).with_fixed_bars(false, true), ]; - let ui = Adapt::new(tree, data).on_message(|_, data, control| data.handle(control)); + let ui = tree + .with_state(data) + .on_message(|_, data, control| data.handle(control)); let window = Window::new(ui, "Dynamic widget demo"); diff --git a/examples/data-list.rs b/examples/data-list.rs index 1ef20f9b8..ff9139337 100644 --- a/examples/data-list.rs +++ b/examples/data-list.rs @@ -22,7 +22,7 @@ use kas::prelude::*; use kas::widgets::edit::{EditBox, EditField, EditGuard}; use kas::widgets::{column, row}; -use kas::widgets::{Adapt, Button, Label, List, RadioButton, ScrollBarRegion, Separator, Text}; +use kas::widgets::{Button, Label, List, RadioButton, ScrollBarRegion, Separator, Text}; #[derive(Debug)] struct SelectEntry(usize); @@ -178,7 +178,9 @@ fn main() -> kas::runner::Result<()> { ScrollBarRegion::new(list).with_fixed_bars(false, true), ]; - let ui = Adapt::new(tree, data).on_message(|_, data, control| data.handle(control)); + let ui = tree + .with_state(data) + .on_message(|_, data, control| data.handle(control)); let window = Window::new(ui, "Dynamic widget demo"); diff --git a/examples/gallery.rs b/examples/gallery.rs index d01403b8e..b1b00c5f8 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -163,19 +163,19 @@ fn widgets() -> Box> { img_light.clone(), ConfigMsg::Theme(ThemeConfigMsg::SetActiveScheme("light".to_string())) ) - .with_color("#B38DF9".parse().unwrap()) + .with_background("#B38DF9".parse().unwrap()) .with_access_key(Key::Character("h".into())), Button::new_msg( img_light, ConfigMsg::Theme(ThemeConfigMsg::SetActiveScheme("blue".to_string())) ) - .with_color("#7CDAFF".parse().unwrap()) + .with_background("#7CDAFF".parse().unwrap()) .with_access_key(Key::Character("b".into())), Button::new_msg( img_dark, ConfigMsg::Theme(ThemeConfigMsg::SetActiveScheme("dark".to_string())) ) - .with_color("#E77346".parse().unwrap()) + .with_background("#E77346".parse().unwrap()) .with_access_key(Key::Character("k".into())), ] .map_any() @@ -234,7 +234,8 @@ fn widgets() -> Box> { row!["Child window", popup_edit_box], ]; - let ui = Adapt::new(widgets, data) + let ui = widgets + .with_state(data) .on_message(|_, data, ScrollMsg(value)| { println!("ScrollMsg({value})"); data.ratio = value as f32 / 100.0; @@ -340,7 +341,8 @@ Demonstration of *as-you-type* formatting from **Markdown**. }), ]; - let ui = Adapt::new(ui, Data::default()) + let ui = ui + .with_state(Data::default()) .on_update(|_, data, app_data: &AppData| data.disabled = app_data.disabled) .on_message(|_, data, MsgDirection| { data.dir = match data.dir { @@ -409,7 +411,8 @@ fn filter_list() -> Box> { sel_buttons.map(|data: &Data| &data.mode), ScrollBars::new(list_view), ]; - let ui = Adapt::new(ui, data) + let ui = ui + .with_state(data) .on_message(|_, data, mode| data.mode = mode) .on_message(|_, data, selection: SelectionMsg| match selection { SelectionMsg::Select(i) => println!("Selected: {}", &data.list[i]), @@ -601,28 +604,30 @@ fn main() -> kas::runner::Result<()> { .with_msg(|_, title| WindowCommand::SetTitle(format!("Gallery — {}", title))), ]; - let ui = Adapt::new(ui, AppData::default()).on_message(|cx, state, msg| match msg { - Menu::Theme(name) => { - println!("Theme: {name:?}"); - let act = cx - .config() - .update_theme(|theme| theme.set_active_theme(name)); - cx.window_action(act); - } - Menu::Colour(name) => { - println!("Colour scheme: {name:?}"); - let act = cx - .config() - .update_theme(|theme| theme.set_active_scheme(name)); - cx.window_action(act); - } - Menu::Disabled(disabled) => { - state.disabled = disabled; - } - Menu::Quit => { - cx.exit(); - } - }); + let ui = ui + .with_state(AppData::default()) + .on_message(|cx, state, msg| match msg { + Menu::Theme(name) => { + println!("Theme: {name:?}"); + let act = cx + .config() + .update_theme(|theme| theme.set_active_theme(name)); + cx.window_action(act); + } + Menu::Colour(name) => { + println!("Colour scheme: {name:?}"); + let act = cx + .config() + .update_theme(|theme| theme.set_active_scheme(name)); + cx.window_action(act); + } + Menu::Disabled(disabled) => { + state.disabled = disabled; + } + Menu::Quit => { + cx.exit(); + } + }); runner.add(Window::new(ui, "Gallery — Widgets")); runner.run() diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs index b42b02a54..8036e83ce 100644 --- a/examples/stopwatch.rs +++ b/examples/stopwatch.rs @@ -9,7 +9,7 @@ use std::time::{Duration, Instant}; use kas::decorations::Decorations; use kas::prelude::*; -use kas::widgets::{format_data, row, Adapt, Button}; +use kas::widgets::{format_data, row, Button}; #[derive(Clone, Debug)] struct MsgReset; @@ -29,7 +29,7 @@ fn make_window() -> impl Widget { Button::label_msg("&start / &stop", MsgStart).map_any(), ]; - Adapt::new(ui, Timer::default()) + ui.with_state(Timer::default()) .on_configure(|cx, _| cx.enable_alt_bypass(true)) .on_message(|_, timer, MsgReset| *timer = Timer::default()) .on_message(|cx, timer, MsgStart| { diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs index 7e787254e..062ade4e8 100644 --- a/examples/sync-counter.rs +++ b/examples/sync-counter.rs @@ -7,7 +7,7 @@ //! //! Each window shares the counter, but has its own increment step. -use kas::widgets::{column, format_data, row, Adapt, Button, Label, Slider}; +use kas::widgets::{column, format_data, row, AdaptWidget, Button, Label, Slider}; use kas::{messages::MessageStack, Window}; #[derive(Clone, Debug)] @@ -46,7 +46,8 @@ fn counter(title: &str) -> Window { ], ]; - let ui = Adapt::new(ui, initial) + let ui = ui + .with_state(initial) .on_update(|_, state, count| state.0 = *count) .on_message(|_, state, SetValue(v)| state.1 = v); Window::new(ui, title) diff --git a/examples/times-tables.rs b/examples/times-tables.rs index 1bfbfd7c2..f85f283e0 100644 --- a/examples/times-tables.rs +++ b/examples/times-tables.rs @@ -2,7 +2,7 @@ use kas::prelude::*; use kas::view::{driver, MatrixData, MatrixView, SelectionMode, SelectionMsg, SharedData}; -use kas::widgets::{column, row, Adapt, EditBox, ScrollBars}; +use kas::widgets::{column, row, EditBox, ScrollBars}; #[derive(Debug)] struct TableSize(usize); @@ -63,7 +63,8 @@ fn main() -> kas::runner::Result<()> { ], table.align(AlignHints::RIGHT), ]; - let ui = Adapt::new(ui, TableSize(12)) + let ui = ui + .with_state(TableSize(12)) .on_message(|_, data, SetLen(len)| data.0 = len) .on_message(|_, _, selection| match selection { SelectionMsg::<(usize, usize)>::Select((col, row)) => { diff --git a/tests/layout_macros.rs b/tests/layout_macros.rs index 7378c69df..a5b931768 100644 --- a/tests/layout_macros.rs +++ b/tests/layout_macros.rs @@ -16,7 +16,7 @@ fn row() { #[test] fn list() { - use_widget(list!(left, ["one", "two"])); + use_widget(list!["one", "two"].with_direction(kas::dir::Left)); } #[test]