diff options
author | 2024-05-23 13:29:45 +0200 | |
---|---|---|
committer | 2024-05-23 13:29:45 +0200 | |
commit | d8ba6b0673a33724a177f3a1ba59705527280142 (patch) | |
tree | 89482c8d1e3a03e00b3a8151abbb81e30ae5898c /widget | |
parent | 72ed8bcc8def9956e25f3720a3095fc96bb2eef0 (diff) | |
parent | 468794d918eb06c1dbebb33c32b10017ad335f05 (diff) | |
download | iced-d8ba6b0673a33724a177f3a1ba59705527280142.tar.gz iced-d8ba6b0673a33724a177f3a1ba59705527280142.tar.bz2 iced-d8ba6b0673a33724a177f3a1ba59705527280142.zip |
Merge branch 'master' into feat/text-macro
Diffstat (limited to 'widget')
40 files changed, 2693 insertions, 1317 deletions
diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3c9ffddb..3c9f6a54 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -21,12 +24,14 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] +advanced = [] [dependencies] iced_renderer.workspace = true iced_runtime.workspace = true num-traits.workspace = true +rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 5790f811..dc949671 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -49,6 +49,7 @@ use crate::core::{ pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Renderer: crate::core::Renderer, + Theme: Catalog, { content: Element<'a, Message, Theme, Renderer>, on_press: Option<Message>, @@ -56,20 +57,18 @@ where height: Length, padding: Padding, clip: bool, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where Renderer: crate::core::Renderer, + Theme: Catalog, { /// Creates a new [`Button`] with the given content. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self - where - Theme: DefaultStyle + 'a, - { + ) -> Self { let content = content.into(); let size = content.as_widget().size_hint(); @@ -80,7 +79,7 @@ where height: size.height.fluid(), padding: DEFAULT_PADDING, clip: false, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -119,21 +118,30 @@ where self } - /// Sets the style variant of this [`Button`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); - self - } - /// Sets whether the contents of the [`Button`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { self.clip = clip; self } + + /// Sets the style of the [`Button`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Button`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -146,6 +154,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where Message: 'a + Clone, Renderer: 'a + crate::core::Renderer, + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -304,19 +313,19 @@ where Status::Active }; - let styling = (self.style)(theme, status); + let style = theme.style(&self.class, status); - if styling.background.is_some() - || styling.border.width > 0.0 - || styling.shadow.color.a > 0.0 + if style.background.is_some() + || style.border.width > 0.0 + || style.shadow.color.a > 0.0 { renderer.fill_quad( renderer::Quad { bounds, - border: styling.border, - shadow: styling.shadow, + border: style.border, + shadow: style.shadow, }, - styling + style .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); @@ -333,7 +342,7 @@ where renderer, theme, &renderer::Style { - text_color: styling.text_color, + text_color: style.text_color, }, content_layout, cursor, @@ -378,7 +387,7 @@ impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: Clone + 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: crate::core::Renderer + 'a, { fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { @@ -407,9 +416,9 @@ pub enum Status { Disabled, } -/// The appearance of a button. +/// The style of a button. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the button. pub background: Option<Background>, /// The text [`Color`] of the button. @@ -420,8 +429,8 @@ pub struct Appearance { pub shadow: Shadow, } -impl Appearance { - /// Updates the [`Appearance`] with the given [`Background`]. +impl Style { + /// Updates the [`Style`] with the given [`Background`]. pub fn with_background(self, background: impl Into<Background>) -> Self { Self { background: Some(background.into()), @@ -430,7 +439,7 @@ impl Appearance { } } -impl std::default::Default for Appearance { +impl Default for Style { fn default() -> Self { Self { background: None, @@ -441,41 +450,41 @@ impl std::default::Default for Appearance { } } -/// The style of a [`Button`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Button`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Button`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Button`]. - fn default_style(&self, status: Status) -> Appearance; -} + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - primary(self, status) - } + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self +/// A styling function for a [`Button`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) } -} -impl DefaultStyle for Color { - fn default_style(&self, _status: Status) -> Appearance { - Appearance::default().with_background(*self) + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// A primary button; denoting a main action. -pub fn primary(theme: &Theme, status: Status) -> Appearance { +pub fn primary(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let base = styled(palette.primary.strong); match status { Status::Active | Status::Pressed => base, - Status::Hovered => Appearance { + Status::Hovered => Style { background: Some(Background::Color(palette.primary.base.color)), ..base }, @@ -484,13 +493,13 @@ pub fn primary(theme: &Theme, status: Status) -> Appearance { } /// A secondary button; denoting a complementary action. -pub fn secondary(theme: &Theme, status: Status) -> Appearance { +pub fn secondary(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let base = styled(palette.secondary.base); match status { Status::Active | Status::Pressed => base, - Status::Hovered => Appearance { + Status::Hovered => Style { background: Some(Background::Color(palette.secondary.strong.color)), ..base }, @@ -499,13 +508,13 @@ pub fn secondary(theme: &Theme, status: Status) -> Appearance { } /// A success button; denoting a good outcome. -pub fn success(theme: &Theme, status: Status) -> Appearance { +pub fn success(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let base = styled(palette.success.base); match status { Status::Active | Status::Pressed => base, - Status::Hovered => Appearance { + Status::Hovered => Style { background: Some(Background::Color(palette.success.strong.color)), ..base }, @@ -514,13 +523,13 @@ pub fn success(theme: &Theme, status: Status) -> Appearance { } /// A danger button; denoting a destructive action. -pub fn danger(theme: &Theme, status: Status) -> Appearance { +pub fn danger(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let base = styled(palette.danger.base); match status { Status::Active | Status::Pressed => base, - Status::Hovered => Appearance { + Status::Hovered => Style { background: Some(Background::Color(palette.danger.strong.color)), ..base }, @@ -529,17 +538,17 @@ pub fn danger(theme: &Theme, status: Status) -> Appearance { } /// A text button; useful for links. -pub fn text(theme: &Theme, status: Status) -> Appearance { +pub fn text(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let base = Appearance { + let base = Style { text_color: palette.background.base.text, - ..Appearance::default() + ..Style::default() }; match status { Status::Active | Status::Pressed => base, - Status::Hovered => Appearance { + Status::Hovered => Style { text_color: palette.background.base.text.scale_alpha(0.8), ..base }, @@ -547,21 +556,21 @@ pub fn text(theme: &Theme, status: Status) -> Appearance { } } -fn styled(pair: palette::Pair) -> Appearance { - Appearance { +fn styled(pair: palette::Pair) -> Style { + Style { background: Some(Background::Color(pair.color)), text_color: pair.text, border: Border::rounded(2), - ..Appearance::default() + ..Style::default() } } -fn disabled(appearance: Appearance) -> Appearance { - Appearance { - background: appearance +fn disabled(style: Style) -> Style { + Style { + background: style .background .map(|background| background.scale_alpha(0.5)), - text_color: appearance.text_color.scale_alpha(0.5), - ..appearance + text_color: style.text_color.scale_alpha(0.5), + ..style } } diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 0eda0191..be09f163 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,8 +6,11 @@ mod program; pub use event::Event; pub use program::Program; -pub use crate::graphics::geometry::*; -pub use crate::renderer::geometry::*; +pub use crate::graphics::cache::Group; +pub use crate::graphics::geometry::{ + fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, + Path, Stroke, Style, Text, +}; use crate::core; use crate::core::layout::{self, Layout}; @@ -15,12 +18,25 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Length, Rectangle, Shell, Size, Transformation, Widget, + Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; use crate::graphics::geometry; use std::marker::PhantomData; +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +pub type Cache<Renderer = crate::Renderer> = geometry::Cache<Renderer>; + +/// The geometry supported by a renderer. +pub type Geometry<Renderer = crate::Renderer> = + <Renderer as geometry::Renderer>::Geometry; + +/// The frame supported by a renderer. +pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>; + /// A widget capable of drawing 2D graphics. /// /// ## Drawing a simple circle @@ -42,7 +58,7 @@ use std::marker::PhantomData; /// impl Program<()> for Circle { /// type State = (); /// -/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry>{ +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry> { /// // We prepare a new `Frame` /// let mut frame = Frame::new(renderer, bounds.size()); /// @@ -207,12 +223,15 @@ where let state = tree.state.downcast_ref::<P::State>(); - renderer.with_transformation( - Transformation::translate(bounds.x, bounds.y), + renderer.with_translation( + Vector::new(bounds.x, bounds.y), |renderer| { - renderer.draw( - self.program.draw(state, renderer, theme, bounds, cursor), - ); + let layers = + self.program.draw(state, renderer, theme, bounds, cursor); + + for layer in layers { + renderer.draw_geometry(layer); + } }, ); } diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index 0bff4bda..a7ded0f4 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,5 +1,6 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; +use crate::canvas::Geometry; use crate::core::Rectangle; use crate::graphics::geometry; @@ -52,7 +53,7 @@ where theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Renderer::Geometry>; + ) -> Vec<Geometry<Renderer>>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -94,7 +95,7 @@ where theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Renderer::Geometry> { + ) -> Vec<Geometry<Renderer>> { T::draw(self, state, renderer, theme, bounds, cursor) } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 15fb8f58..225c316d 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -39,6 +39,7 @@ pub struct Checkbox< Renderer = crate::Renderer, > where Renderer: text::Renderer, + Theme: Catalog, { is_checked: bool, on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>, @@ -51,12 +52,13 @@ pub struct Checkbox< text_shaping: text::Shaping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> where Renderer: text::Renderer, + Theme: Catalog, { /// The default size of a [`Checkbox`]. const DEFAULT_SIZE: f32 = 16.0; @@ -69,10 +71,7 @@ where /// It expects: /// * the label of the [`Checkbox`] /// * a boolean describing whether the [`Checkbox`] is checked or not - pub fn new(label: impl Into<String>, is_checked: bool) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(label: impl Into<String>, is_checked: bool) -> Self { Checkbox { is_checked, on_toggle: None, @@ -91,7 +90,7 @@ where line_height: text::LineHeight::default(), shaping: text::Shaping::Basic, }, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -174,11 +173,20 @@ where } /// Sets the style of the [`Checkbox`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Checkbox`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -187,6 +195,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Checkbox<'a, Message, Theme, Renderer> where Renderer: text::Renderer, + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() @@ -285,7 +294,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -304,7 +313,7 @@ where Status::Active { is_checked } }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); { let layout = children.next().unwrap(); @@ -313,10 +322,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); let Icon { @@ -331,7 +340,7 @@ where if self.is_checked { renderer.fill_text( text::Text { - content: &code_point.to_string(), + content: code_point.to_string(), font: *font, size, line_height: *line_height, @@ -341,7 +350,7 @@ where shaping: *shaping, }, bounds.center(), - appearance.icon_color, + style.icon_color, *viewport, ); } @@ -352,11 +361,11 @@ where crate::text::draw( renderer, - style, + defaults, label_layout, tree.state.downcast_ref(), - crate::text::Appearance { - color: appearance.text_color, + crate::text::Style { + color: style.text_color, }, viewport, ); @@ -368,7 +377,7 @@ impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, + Theme: 'a + Catalog, Renderer: 'a + text::Renderer, { fn from( @@ -413,9 +422,9 @@ pub enum Status { }, } -/// The appearance of a checkbox. +/// The style of a checkbox. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the checkbox. pub background: Background, /// The icon [`Color`] of the checkbox. @@ -426,29 +435,37 @@ pub struct Appearance { pub text_color: Option<Color>, } -/// The style of a [`Checkbox`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Checkbox`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Checkbox`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Checkbox`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - primary(self, status) +/// A styling function for a [`Checkbox`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// A primary checkbox; denoting a main toggle. -pub fn primary(theme: &Theme, status: Status) -> Appearance { +pub fn primary(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); match status { @@ -474,7 +491,7 @@ pub fn primary(theme: &Theme, status: Status) -> Appearance { } /// A secondary checkbox; denoting a complementary toggle. -pub fn secondary(theme: &Theme, status: Status) -> Appearance { +pub fn secondary(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); match status { @@ -500,7 +517,7 @@ pub fn secondary(theme: &Theme, status: Status) -> Appearance { } /// A success checkbox; denoting a positive toggle. -pub fn success(theme: &Theme, status: Status) -> Appearance { +pub fn success(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); match status { @@ -526,7 +543,7 @@ pub fn success(theme: &Theme, status: Status) -> Appearance { } /// A danger checkbox; denoting a negaive toggle. -pub fn danger(theme: &Theme, status: Status) -> Appearance { +pub fn danger(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); match status { @@ -556,8 +573,8 @@ fn styled( base: palette::Pair, accent: palette::Pair, is_checked: bool, -) -> Appearance { - Appearance { +) -> Style { + Style { background: Background::Color(if is_checked { accent.color } else { diff --git a/widget/src/column.rs b/widget/src/column.rs index d37ef695..df7829b3 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -33,11 +33,18 @@ where Self::from_vec(Vec::new()) } + /// Creates a [`Column`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + /// Creates a [`Column`] with the given elements. pub fn with_children( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, ) -> Self { - Self::new().extend(children) + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) } /// Creates a [`Column`] from an already allocated [`Vec`]. diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index ee24d742..253850df 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -32,6 +32,7 @@ pub struct ComboBox< Theme = crate::Theme, Renderer = crate::Renderer, > where + Theme: Catalog, Renderer: text::Renderer, { state: &'a State<T>, @@ -42,7 +43,7 @@ pub struct ComboBox< on_option_hovered: Option<Box<dyn Fn(T) -> Message>>, on_close: Option<Message>, on_input: Option<Box<dyn Fn(String) -> Message>>, - menu_style: menu::Style<'a, Theme>, + menu_class: <Theme as menu::Catalog>::Class<'a>, padding: Padding, size: Option<f32>, } @@ -50,6 +51,7 @@ pub struct ComboBox< impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, + Theme: Catalog, Renderer: text::Renderer, { /// Creates a new [`ComboBox`] with the given list of options, a placeholder, @@ -60,18 +62,10 @@ where placeholder: &str, selection: Option<&T>, on_selected: impl Fn(T) -> Message + 'static, - ) -> Self - where - Theme: DefaultStyle + 'a, - { - let style = Theme::default_style(); - - let text_input = TextInput::with_style( - placeholder, - &state.value(), - style.text_input, - ) - .on_input(TextInputEvent::TextChanged); + ) -> Self { + let text_input = TextInput::new(placeholder, &state.value()) + .on_input(TextInputEvent::TextChanged) + .class(Theme::default_input()); let selection = selection.map(T::to_string).unwrap_or_default(); @@ -84,7 +78,7 @@ where on_option_hovered: None, on_input: None, on_close: None, - menu_style: style.menu, + menu_class: <Theme as Catalog>::default_menu(), padding: text_input::DEFAULT_PADDING, size: None, } @@ -124,18 +118,6 @@ where self } - /// Sets the style of the [`ComboBox`]. - pub fn style(mut self, style: impl Into<Style<'a, Theme>>) -> Self - where - Theme: 'a, - { - let style = style.into(); - - self.text_input = self.text_input.style(style.text_input); - self.menu_style = style.menu; - self - } - /// Sets the [`Renderer::Font`] of the [`ComboBox`]. /// /// [`Renderer::Font`]: text::Renderer @@ -173,6 +155,55 @@ where ..self } } + + /// Sets the style of the input of the [`ComboBox`]. + #[must_use] + pub fn input_style( + mut self, + style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a, + ) -> Self + where + <Theme as text_input::Catalog>::Class<'a>: + From<text_input::StyleFn<'a, Theme>>, + { + self.text_input = self.text_input.style(style); + self + } + + /// Sets the style of the menu of the [`ComboBox`]. + #[must_use] + pub fn menu_style( + mut self, + style: impl Fn(&Theme) -> menu::Style + 'a, + ) -> Self + where + <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>, + { + self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the input of the [`ComboBox`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn input_class( + mut self, + class: impl Into<<Theme as text_input::Catalog>::Class<'a>>, + ) -> Self { + self.text_input = self.text_input.class(class); + self + } + + /// Sets the style class of the menu of the [`ComboBox`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn menu_class( + mut self, + class: impl Into<<Theme as menu::Catalog>::Class<'a>>, + ) -> Self { + self.menu_class = class.into(); + self + } } /// The local state of a [`ComboBox`]. @@ -296,6 +327,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -668,38 +700,47 @@ where .. } = tree.state.downcast_mut::<Menu<T>>(); - let bounds = layout.bounds(); - self.state.sync_filtered_options(filtered_options); - let mut menu = menu::Menu::new( - menu, - &filtered_options.options, - hovered_option, - |x| { - tree.children[0] - .state - .downcast_mut::<text_input::State<Renderer::Paragraph>>( - ) - .unfocus(); - - (self.on_selected)(x) - }, - self.on_option_hovered.as_deref(), - &self.menu_style, - ) - .width(bounds.width) - .padding(self.padding); - - if let Some(font) = self.font { - menu = menu.font(font); - } + if filtered_options.options.is_empty() { + None + } else { + let bounds = layout.bounds(); + + let mut menu = menu::Menu::new( + menu, + &filtered_options.options, + hovered_option, + |x| { + tree.children[0] + .state + .downcast_mut::<text_input::State<Renderer::Paragraph>>( + ) + .unfocus(); + + (self.on_selected)(x) + }, + self.on_option_hovered.as_deref(), + &self.menu_class, + ) + .width(bounds.width) + .padding(self.padding); + + if let Some(font) = self.font { + menu = menu.font(font); + } - if let Some(size) = self.size { - menu = menu.text_size(size); - } + if let Some(size) = self.size { + menu = menu.text_size(size); + } - Some(menu.overlay(layout.position() + translation, bounds.height)) + Some( + menu.overlay( + layout.position() + translation, + bounds.height, + ), + ) + } } else { None } @@ -712,7 +753,7 @@ impl<'a, T, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone + 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self { @@ -720,6 +761,21 @@ where } } +/// The theme catalog of a [`ComboBox`]. +pub trait Catalog: text_input::Catalog + menu::Catalog { + /// The default class for the text input of the [`ComboBox`]. + fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> { + <Self as text_input::Catalog>::default() + } + + /// The default class for the menu of the [`ComboBox`]. + fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> { + <Self as menu::Catalog>::default() + } +} + +impl Catalog for Theme {} + fn search<'a, T, A>( options: impl IntoIterator<Item = T> + 'a, option_matchers: impl IntoIterator<Item = &'a A> + 'a, @@ -762,30 +818,3 @@ where }) .collect() } - -/// The style of a [`ComboBox`]. -#[allow(missing_debug_implementations)] -pub struct Style<'a, Theme> { - /// The style of the [`TextInput`] of the [`ComboBox`]. - pub text_input: text_input::Style<'a, Theme>, - - /// The style of the [`Menu`] of the [`ComboBox`]. - /// - /// [`Menu`]: menu::Menu - pub menu: menu::Style<'a, Theme>, -} - -/// The default style of a [`ComboBox`]. -pub trait DefaultStyle: Sized { - /// Returns the default style of a [`ComboBox`]. - fn default_style() -> Style<'static, Self>; -} - -impl DefaultStyle for Theme { - fn default_style() -> Style<'static, Self> { - Style { - text_input: Box::new(text_input::default), - menu: menu::DefaultStyle::default_style(), - } - } -} diff --git a/widget/src/container.rs b/widget/src/container.rs index 7c133588..51967707 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -9,8 +9,9 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, - Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, + self, Background, Border, Clipboard, Color, Element, Layout, Length, + Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, + Widget, }; use crate::runtime::Command; @@ -24,7 +25,8 @@ pub struct Container< Theme = crate::Theme, Renderer = crate::Renderer, > where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { id: Option<Id>, padding: Padding, @@ -36,27 +38,17 @@ pub struct Container< vertical_alignment: alignment::Vertical, clip: bool, content: Element<'a, Message, Theme, Renderer>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { /// Creates a [`Container`] with the given content. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self - where - Theme: DefaultStyle + 'a, - { - Self::with_style(content, Theme::default_style) - } - - /// Creates a [`Container`] with the given content and style. - pub fn with_style( - content: impl Into<Element<'a, Message, Theme, Renderer>>, - style: impl Fn(&Theme, Status) -> Appearance + 'a, ) -> Self { let content = content.into(); let size = content.as_widget().size_hint(); @@ -71,7 +63,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, clip: false, - style: Box::new(style), + class: Theme::default(), content, } } @@ -100,6 +92,46 @@ where self } + /// Sets the [`Container`] to fill the available space in the horizontal axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_x`]. + /// + /// Calling this method is equivalent to calling [`width`] with a + /// [`Length::Fill`]. + /// + /// [`center_x`]: Self::center_x + /// [`width`]: Self::width + pub fn fill_x(self) -> Self { + self.width(Length::Fill) + } + + /// Sets the [`Container`] to fill the available space in the vetical axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_y`]. + /// + /// Calling this method is equivalent to calling [`height`] with a + /// [`Length::Fill`]. + /// + /// [`center_y`]: Self::center_x + /// [`height`]: Self::height + pub fn fill_y(self) -> Self { + self.height(Length::Fill) + } + + /// Sets the [`Container`] to fill all the available space. + /// + /// Calling this method is equivalent to chaining [`fill_x`] and + /// [`fill_y`]. + /// + /// [`center`]: Self::center + /// [`fill_x`]: Self::fill_x + /// [`fill_y`]: Self::fill_y + pub fn fill(self) -> Self { + self.width(Length::Fill).height(Length::Fill) + } + /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { self.max_width = max_width.into().0; @@ -124,25 +156,27 @@ where self } - /// Centers the contents in the horizontal axis of the [`Container`]. - pub fn center_x(mut self) -> Self { - self.horizontal_alignment = alignment::Horizontal::Center; - self + /// Sets the width of the [`Container`] and centers its contents horizontally. + pub fn center_x(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Center) } - /// Centers the contents in the vertical axis of the [`Container`]. - pub fn center_y(mut self) -> Self { - self.vertical_alignment = alignment::Vertical::Center; - self + /// Sets the height of the [`Container`] and centers its contents vertically. + pub fn center_y(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Center) } - /// Sets the style of the [`Container`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); - self + /// Centers the contents in both the horizontal and vertical axes of the + /// [`Container`]. + /// + /// This is equivalent to chaining [`center_x`] and [`center_y`]. + /// + /// [`center_x`]: Self::center_x + /// [`center_y`]: Self::center_y + pub fn center(self, length: impl Into<Length>) -> Self { + let length = length.into(); + + self.center_x(length).center_y(length) } /// Sets whether the contents of the [`Container`] should be clipped on @@ -151,12 +185,31 @@ where self.clip = clip; self } + + /// Sets the style of the [`Container`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Container`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Container<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { self.content.as_widget().tag() @@ -272,14 +325,7 @@ where viewport: &Rectangle, ) { let bounds = layout.bounds(); - - let status = if cursor.is_over(bounds) { - Status::Hovered - } else { - Status::Idle - }; - - let style = (self.style)(theme, status); + let style = theme.style(&self.class); if let Some(clipped_viewport) = bounds.intersection(viewport) { draw_background(renderer, &style, bounds); @@ -324,8 +370,8 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, - Renderer: 'a + crate::core::Renderer, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( column: Container<'a, Message, Theme, Renderer>, @@ -362,25 +408,25 @@ pub fn layout( ) } -/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. +/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. pub fn draw_background<Renderer>( renderer: &mut Renderer, - appearance: &Appearance, + style: &Style, bounds: Rectangle, ) where - Renderer: crate::core::Renderer, + Renderer: core::Renderer, { - if appearance.background.is_some() - || appearance.border.width > 0.0 - || appearance.shadow.color.a > 0.0 + if style.background.is_some() + || style.border.width > 0.0 + || style.shadow.color.a > 0.0 { renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, - shadow: appearance.shadow, + border: style.border, + shadow: style.shadow, }, - appearance + style .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); @@ -502,7 +548,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { /// The appearance of a container. #[derive(Debug, Clone, Copy, Default)] -pub struct Appearance { +pub struct Style { /// The text [`Color`] of the container. pub text_color: Option<Color>, /// The [`Background`] of the container. @@ -513,8 +559,8 @@ pub struct Appearance { pub shadow: Shadow, } -impl Appearance { - /// Updates the border of the [`Appearance`] with the given [`Color`] and `width`. +impl Style { + /// Updates the border of the [`Style`] with the given [`Color`] and `width`. pub fn with_border( self, color: impl Into<Color>, @@ -530,7 +576,7 @@ impl Appearance { } } - /// Updates the background of the [`Appearance`]. + /// Updates the background of the [`Style`]. pub fn with_background(self, background: impl Into<Background>) -> Self { Self { background: Some(background.into()), @@ -539,99 +585,78 @@ impl Appearance { } } -impl From<Color> for Appearance { +impl From<Color> for Style { fn from(color: Color) -> Self { Self::default().with_background(color) } } -impl From<Gradient> for Appearance { +impl From<Gradient> for Style { fn from(gradient: Gradient) -> Self { Self::default().with_background(gradient) } } -impl From<gradient::Linear> for Appearance { +impl From<gradient::Linear> for Style { fn from(gradient: gradient::Linear) -> Self { Self::default().with_background(gradient) } } -/// The possible status of a [`Container`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Status { - /// The [`Container`] is idle. - Idle, - /// The [`Container`] is being hovered. - Hovered, -} +/// The theme catalog of a [`Container`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The style of a [`Container`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; -/// The default style of a [`Container`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Container`]. - fn default_style(&self, status: Status) -> Appearance; + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - transparent(self, status) - } -} +/// A styling function for a [`Container`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self - } -} +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; -impl DefaultStyle for Color { - fn default_style(&self, _status: Status) -> Appearance { - Appearance::from(*self) + fn default<'a>() -> Self::Class<'a> { + Box::new(transparent) } -} - -impl DefaultStyle for Gradient { - fn default_style(&self, _status: Status) -> Appearance { - Appearance::from(*self) - } -} -impl DefaultStyle for gradient::Linear { - fn default_style(&self, _status: Status) -> Appearance { - Appearance::from(*self) + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } /// A transparent [`Container`]. -pub fn transparent<Theme>(_theme: &Theme, _status: Status) -> Appearance { - Appearance::default() +pub fn transparent<Theme>(_theme: &Theme) -> Style { + Style::default() } /// A rounded [`Container`] with a background. -pub fn rounded_box(theme: &Theme, _status: Status) -> Appearance { +pub fn rounded_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); - Appearance { + Style { background: Some(palette.background.weak.color.into()), border: Border::rounded(2), - ..Appearance::default() + ..Style::default() } } /// A bordered [`Container`] with a background. -pub fn bordered_box(theme: &Theme, _status: Status) -> Appearance { +pub fn bordered_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); - Appearance { + Style { background: Some(palette.background.weak.color.into()), border: Border { width: 1.0, radius: 0.0.into(), color: palette.background.strong.color, }, - ..Appearance::default() + ..Style::default() } } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index b294a1d4..016bafbb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -5,8 +5,9 @@ use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; -use crate::core::{Element, Length, Pixels}; +use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; +use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; @@ -20,7 +21,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Themer}; +use crate::{Column, MouseArea, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -51,6 +52,19 @@ macro_rules! row { ); } +/// Creates a [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +#[macro_export] +macro_rules! stack { + () => ( + $crate::Stack::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Stack::with_children([$($crate::core::Element::from($x)),+]) + ); +} + /// Creates a new [`Text`] widget with the provided content. /// /// [`Text`]: core::widget::Text @@ -104,12 +118,34 @@ pub fn container<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Container<'a, Message, Theme, Renderer> where - Theme: container::DefaultStyle + 'a, + Theme: container::Catalog + 'a, Renderer: core::Renderer, { Container::new(content) } +/// Creates a new [`Container`] that fills all the available space +/// and centers its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let centered = container("Centered!").center(Length::Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn center<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).center(Length::Fill) +} + /// Creates a new [`Column`] with the given children. pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, @@ -143,6 +179,428 @@ where Row::with_children(children) } +/// Creates a new [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +pub fn stack<'a, Message, Theme, Renderer>( + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, +) -> Stack<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Stack::with_children(children) +} + +/// Wraps the given widget and captures any mouse button presses inside the bounds of +/// the widget—effectively making it _opaque_. +/// +/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse +/// events from passing through layers. +/// +/// [`Stack`]: crate::Stack +pub fn opaque<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Opaque<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + } + + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Opaque<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec<Tree> { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size<Length> { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size<Length> { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation<Message>, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let is_mouse_press = matches!( + event, + core::Event::Mouse(mouse::Event::ButtonPressed(_)) + ); + + if let core::event::Status::Captured = + self.content.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ) + { + return event::Status::Captured; + } + + if is_mouse_press && cursor.is_over(layout.bounds()) { + event::Status::Captured + } else { + event::Status::Ignored + } + } + + fn mouse_interaction( + &self, + state: &core::widget::Tree, + layout: core::Layout<'_>, + cursor: core::mouse::Cursor, + viewport: &core::Rectangle, + renderer: &Renderer, + ) -> core::mouse::Interaction { + let interaction = self + .content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer); + + if interaction == mouse::Interaction::None + && cursor.is_over(layout.bounds()) + { + mouse::Interaction::Idle + } else { + interaction + } + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>> + { + self.content.as_widget_mut().overlay( + state, + layout, + renderer, + translation, + ) + } + } + + Element::new(Opaque { + content: content.into(), + }) +} + +/// Displays a widget on top of another one, only when the base widget is hovered. +/// +/// This works analogously to a [`stack`], but it will only display the layer on top +/// when the cursor is over the base. It can be useful for removing visual clutter. +/// +/// [`stack`]: stack() +pub fn hover<'a, Message, Theme, Renderer>( + base: impl Into<Element<'a, Message, Theme, Renderer>>, + top: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Hover<'a, Message, Theme, Renderer> { + base: Element<'a, Message, Theme, Renderer>, + top: Element<'a, Message, Theme, Renderer>, + is_top_overlay_active: bool, + } + + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Hover<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + struct Tag; + tree::Tag::of::<Tag>() + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.base), Tree::new(&self.top)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.base, &self.top]); + } + + fn size(&self) -> Size<Length> { + self.base.as_widget().size() + } + + fn size_hint(&self) -> Size<Length> { + self.base.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let base = self.base.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let top = self.top.as_widget().layout( + &mut tree.children[1], + renderer, + &layout::Limits::new(Size::ZERO, base.size()), + ); + + layout::Node::with_children(base.size(), vec![base, top]) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(bounds) = layout.bounds().intersection(viewport) { + let mut children = layout.children().zip(&tree.children); + + let (base_layout, base_tree) = children.next().unwrap(); + + self.base.as_widget().draw( + base_tree, + renderer, + theme, + style, + base_layout, + cursor, + viewport, + ); + + if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + { + let (top_layout, top_tree) = children.next().unwrap(); + + renderer.with_layer(bounds, |renderer| { + self.top.as_widget().draw( + top_tree, renderer, theme, style, top_layout, + cursor, viewport, + ); + }); + } + } + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation<Message>, + ) { + let children = [&self.base, &self.top] + .into_iter() + .zip(layout.children().zip(&mut tree.children)); + + for (child, (layout, tree)) in children { + child.as_widget().operate(tree, layout, renderer, operation); + } + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let mut children = layout.children().zip(&mut tree.children); + let (base_layout, base_tree) = children.next().unwrap(); + + let top_status = if matches!( + event, + Event::Mouse( + mouse::Event::CursorMoved { .. } + | mouse::Event::ButtonReleased(_) + ) + ) || cursor.is_over(layout.bounds()) + { + let (top_layout, top_tree) = children.next().unwrap(); + + self.top.as_widget_mut().on_event( + top_tree, + event.clone(), + top_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } else { + event::Status::Ignored + }; + + if top_status == event::Status::Captured { + return top_status; + } + + self.base.as_widget_mut().on_event( + base_tree, + event.clone(), + base_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + [&self.base, &self.top] + .into_iter() + .rev() + .zip(layout.children().rev().zip(tree.children.iter().rev())) + .map(|(child, (layout, tree))| { + child.as_widget().mouse_interaction( + tree, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::None) + .unwrap_or_default() + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>> + { + let mut overlays = [&mut self.base, &mut self.top] + .into_iter() + .zip(layout.children().zip(tree.children.iter_mut())) + .map(|(child, (layout, tree))| { + child.as_widget_mut().overlay( + tree, + layout, + renderer, + translation, + ) + }); + + if let Some(base_overlay) = overlays.next()? { + return Some(base_overlay); + } + + let top_overlay = overlays.next()?; + self.is_top_overlay_active = top_overlay.is_some(); + + top_overlay + } + } + + Element::new(Hover { + base: base.into(), + top: top.into(), + is_top_overlay_active: false, + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable @@ -150,7 +608,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Scrollable<'a, Message, Theme, Renderer> where - Theme: scrollable::DefaultStyle + 'a, + Theme: scrollable::Catalog + 'a, Renderer: core::Renderer, { Scrollable::new(content) @@ -163,7 +621,7 @@ pub fn button<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Button<'a, Message, Theme, Renderer> where - Theme: button::DefaultStyle + 'a, + Theme: button::Catalog + 'a, Renderer: core::Renderer, { Button::new(content) @@ -180,7 +638,7 @@ pub fn tooltip<'a, Message, Theme, Renderer>( position: tooltip::Position, ) -> crate::Tooltip<'a, Message, Theme, Renderer> where - Theme: container::DefaultStyle + 'a, + Theme: container::Catalog + 'a, Renderer: core::text::Renderer, { Tooltip::new(content, tooltip, position) @@ -190,13 +648,26 @@ where /// /// [`Text`]: core::widget::Text pub fn text<'a, Theme, Renderer>( - text: impl ToString, + text: impl text::IntoFragment<'a>, +) -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: core::text::Renderer, +{ + Text::new(text) +} + +/// Creates a new [`Text`] widget that displays the provided value. +/// +/// [`Text`]: core::widget::Text +pub fn value<'a, Theme, Renderer>( + value: impl ToString, ) -> Text<'a, Theme, Renderer> where - Theme: text::DefaultStyle + 'a, + Theme: text::Catalog + 'a, Renderer: core::text::Renderer, { - Text::new(text.to_string()) + Text::new(value.to_string()) } /// Creates a new [`Checkbox`]. @@ -207,7 +678,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>( is_checked: bool, ) -> Checkbox<'a, Message, Theme, Renderer> where - Theme: checkbox::DefaultStyle + 'a, + Theme: checkbox::Catalog + 'a, Renderer: core::text::Renderer, { Checkbox::new(label, is_checked) @@ -224,7 +695,7 @@ pub fn radio<'a, Message, Theme, Renderer, V>( ) -> Radio<'a, Message, Theme, Renderer> where Message: Clone, - Theme: radio::DefaultStyle + 'a, + Theme: radio::Catalog + 'a, Renderer: core::text::Renderer, V: Copy + Eq, { @@ -240,7 +711,7 @@ pub fn toggler<'a, Message, Theme, Renderer>( f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where - Theme: toggler::DefaultStyle + 'a, + Theme: toggler::Catalog + 'a, Renderer: core::text::Renderer, { Toggler::new(label, is_checked, f) @@ -255,7 +726,7 @@ pub fn text_input<'a, Message, Theme, Renderer>( ) -> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: text_input::DefaultStyle + 'a, + Theme: text_input::Catalog + 'a, Renderer: core::text::Renderer, { TextInput::new(placeholder, value) @@ -269,7 +740,7 @@ pub fn text_editor<'a, Message, Theme, Renderer>( ) -> TextEditor<'a, core::text::highlighter::PlainText, Message, Theme, Renderer> where Message: Clone, - Theme: text_editor::DefaultStyle + 'a, + Theme: text_editor::Catalog + 'a, Renderer: core::text::Renderer, { TextEditor::new(content) @@ -286,7 +757,7 @@ pub fn slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: slider::DefaultStyle + 'a, + Theme: slider::Catalog + 'a, { Slider::new(range, value, on_change) } @@ -302,7 +773,7 @@ pub fn vertical_slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: vertical_slider::DefaultStyle + 'a, + Theme: vertical_slider::Catalog + 'a, { VerticalSlider::new(range, value, on_change) } @@ -320,7 +791,7 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone, - Theme: pick_list::DefaultStyle, + Theme: pick_list::Catalog + overlay::menu::Catalog, Renderer: core::text::Renderer, { PickList::new(options, selected, on_selected) @@ -337,7 +808,7 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>( ) -> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: combo_box::DefaultStyle + 'a, + Theme: combo_box::Catalog + 'a, Renderer: core::text::Renderer, { ComboBox::new(state, placeholder, selection, on_selected) @@ -364,7 +835,7 @@ pub fn vertical_space() -> Space { /// [`Rule`]: crate::Rule pub fn horizontal_rule<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme> where - Theme: rule::DefaultStyle + 'a, + Theme: rule::Catalog + 'a, { Rule::horizontal(height) } @@ -374,7 +845,7 @@ where /// [`Rule`]: crate::Rule pub fn vertical_rule<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme> where - Theme: rule::DefaultStyle + 'a, + Theme: rule::Catalog + 'a, { Rule::vertical(width) } @@ -391,7 +862,7 @@ pub fn progress_bar<'a, Theme>( value: f32, ) -> ProgressBar<'a, Theme> where - Theme: progress_bar::DefaultStyle + 'a, + Theme: progress_bar::Catalog + 'a, { ProgressBar::new(range, value) } @@ -413,7 +884,7 @@ pub fn svg<'a, Theme>( handle: impl Into<core::svg::Handle>, ) -> crate::Svg<'a, Theme> where - Theme: crate::svg::DefaultStyle + 'a, + Theme: crate::svg::Catalog, { crate::Svg::new(handle) } @@ -441,7 +912,7 @@ pub fn qr_code<'a, Theme>( data: &'a crate::qr_code::Data, ) -> crate::QRCode<'a, Theme> where - Theme: crate::qr_code::DefaultStyle + 'a, + Theme: crate::qr_code::Catalog + 'a, { crate::QRCode::new(data) } diff --git a/widget/src/image.rs b/widget/src/image.rs index ccf1f175..80e17263 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -8,11 +8,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; -use std::hash::Hash; - pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. @@ -38,6 +37,8 @@ pub struct Image<Handle> { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, } impl<Handle> Image<Handle> { @@ -47,8 +48,10 @@ impl<Handle> Image<Handle> { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), + rotation: Rotation::default(), + opacity: 1.0, } } @@ -77,6 +80,21 @@ impl<Handle> Image<Handle> { self.filter_method = filter_method; self } + + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Image`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -87,22 +105,24 @@ pub fn layout<Renderer, Handle>( width: Length, height: Length, content_fit: ContentFit, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer<Handle = Handle>, { // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.dimensions(handle); + let image_size = renderer.measure_image(handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); - Size::new(width as f32, height as f32) - }; + // The rotated size of the image + let rotated_size = rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(width, height, image_size); + let raw_size = limits.resolve(width, height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); + let full_size = content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -126,29 +146,47 @@ pub fn draw<Renderer, Handle>( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, ) where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = rotation.apply(image_size); let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); + let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); + let scale = Vector::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + + let final_size = image_size * scale; - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; + let position = match content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; + + let drawing_bounds = Rectangle::new(position, final_size); - renderer.draw(handle.clone(), filter_method, drawing_bounds + offset); + let render = |renderer: &mut Renderer| { + renderer.draw_image( + handle.clone(), + filter_method, + drawing_bounds, + rotation.radians(), + opacity, + ); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height @@ -163,7 +201,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Image<Handle> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { fn size(&self) -> Size<Length> { Size { @@ -185,6 +223,7 @@ where self.width, self.height, self.content_fit, + self.rotation, ) } @@ -204,6 +243,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.opacity, ); } } @@ -212,7 +253,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(image) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 2e3fd713..8fe6f021 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,12 +6,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, + Shell, Size, Vector, Widget, }; -use std::hash::Hash; - /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] pub struct Viewer<Handle> { @@ -94,7 +92,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Viewer<Handle> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -117,7 +115,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_image(&self.handle); let mut size = limits.resolve( self.width, @@ -218,7 +216,7 @@ where event::Status::Captured } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let Some(cursor_position) = cursor.position() else { + let Some(cursor_position) = cursor.position_over(bounds) else { return event::Status::Ignored; }; @@ -304,7 +302,7 @@ where } else if is_mouse_over { mouse::Interaction::Grab } else { - mouse::Interaction::Idle + mouse::Interaction::None } } @@ -335,8 +333,7 @@ where renderer.with_layer(bounds, |renderer| { renderer.with_translation(translation, |renderer| { - image::Renderer::draw( - renderer, + renderer.draw_image( self.handle.clone(), self.filter_method, Rectangle { @@ -344,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + Radians(0.0), + 1.0, ); }); }); @@ -402,7 +401,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>> where Renderer: 'a + image::Renderer<Handle = Handle>, Message: 'a, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(viewer) @@ -421,7 +420,7 @@ pub fn image_size<Renderer>( where Renderer: image::Renderer, { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.measure_image(handle); let (width, height) = { let dimensions = (width as f32, height as f32); diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 8a8d5fe7..fdaadefa 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -40,27 +40,49 @@ where { /// Creates an empty [`Column`]. pub fn new() -> Self { - Column { + Self::from_vecs(Vec::new(), Vec::new()) + } + + /// Creates a [`Column`] from already allocated [`Vec`]s. + /// + /// Keep in mind that the [`Column`] will not inspect the [`Vec`]s, which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Column::width`] or [`Column::height`] accordingly. + pub fn from_vecs( + keys: Vec<Key>, + children: Vec<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { spacing: 0.0, padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, - keys: Vec::new(), - children: Vec::new(), + keys, + children, } } + /// Creates a [`Column`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vecs( + Vec::with_capacity(capacity), + Vec::with_capacity(capacity), + ) + } + /// Creates a [`Column`] with the given elements. pub fn with_children( children: impl IntoIterator< Item = (Key, Element<'a, Message, Theme, Renderer>), >, ) -> Self { - children - .into_iter() - .fold(Self::new(), |column, (key, child)| column.push(key, child)) + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) } /// Sets the vertical spacing _between_ elements. @@ -132,6 +154,18 @@ where self } } + + /// Extends the [`Column`] with the given children. + pub fn extend( + self, + children: impl IntoIterator< + Item = (Key, Element<'a, Message, Theme, Renderer>), + >, + ) -> Self { + children + .into_iter() + .fold(self, |column, (key, child)| column.push(key, child)) + } } impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> @@ -190,7 +224,7 @@ where ); if state.keys != self.keys { - state.keys = self.keys.clone(); + state.keys.clone_from(&self.keys); } } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index eb663ea5..04783dbe 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -18,11 +18,12 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::Element; use crate::core::{ - self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector, }; use crate::runtime::overlay::Nested; use ouroboros::self_referencing; +use rustc_hash::FxHasher; use std::cell::RefCell; use std::hash::{Hash, Hasher as H}; use std::rc::Rc; @@ -106,9 +107,12 @@ where } fn state(&self) -> tree::State { - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let hash = hasher.finish(); + let hash = { + let mut hasher = FxHasher::default(); + self.dependency.hash(&mut hasher); + + hasher.finish() + }; let element = Rc::new(RefCell::new(Some((self.view)(&self.dependency).into()))); @@ -127,9 +131,12 @@ where .state .downcast_mut::<Internal<Message, Theme, Renderer>>(); - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let new_hash = hasher.finish(); + let new_hash = { + let mut hasher = FxHasher::default(); + self.dependency.hash(&mut hasher); + + hasher.finish() + }; if current.hash != new_hash { current.hash = new_hash; diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index a512e0de..7ba71a02 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -478,12 +478,14 @@ where translation: Vector, ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { self.rebuild_element_if_necessary(); + let tree = tree .state .downcast_mut::<Rc<RefCell<Option<Tree>>>>() .borrow_mut() .take() .unwrap(); + let overlay = Overlay(Some( InnerBuilder { instance: self, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 313e1edb..f612102e 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -308,10 +308,13 @@ where content_layout_node.as_ref().unwrap(), ); - element - .as_widget_mut() - .overlay(tree, content_layout, renderer, translation) - .map(|overlay| RefCell::new(Nested::new(overlay))) + ( + element + .as_widget_mut() + .overlay(tree, content_layout, renderer, translation) + .map(|overlay| RefCell::new(Nested::new(overlay))), + content_layout_node, + ) }, } .build(); @@ -341,7 +344,10 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> { #[borrows(mut content, mut tree)] #[not_covariant] - overlay: Option<RefCell<Nested<'this, Message, Theme, Renderer>>>, + overlay: ( + Option<RefCell<Nested<'this, Message, Theme, Renderer>>>, + &'this mut Option<layout::Node>, + ), } impl<'a, 'b, Message, Theme, Renderer> @@ -351,7 +357,7 @@ impl<'a, 'b, Message, Theme, Renderer> &self, f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option<T> { - self.with_overlay(|overlay| { + self.with_overlay(|(overlay, _layout)| { overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) }) } @@ -360,7 +366,7 @@ impl<'a, 'b, Message, Theme, Renderer> &mut self, f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option<T> { - self.with_overlay_mut(|overlay| { + self.with_overlay_mut(|(overlay, _layout)| { overlay.as_mut().map(|nested| (f)(nested.get_mut())) }) } @@ -412,10 +418,27 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event(event, layout, cursor, renderer, clipboard, shell) - }) - .unwrap_or(event::Status::Ignored) + let mut is_layout_invalid = false; + + let event_status = self + .with_overlay_mut_maybe(|overlay| { + let event_status = overlay.on_event( + event, layout, cursor, renderer, clipboard, shell, + ); + + is_layout_invalid = shell.is_layout_invalid(); + + event_status + }) + .unwrap_or(event::Status::Ignored); + + if is_layout_invalid { + self.with_overlay_mut(|(_overlay, layout)| { + **layout = None; + }); + } + + event_status } fn is_over( diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 209dfad9..00e9aaa4 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -2,13 +2,6 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use iced_renderer as renderer; pub use iced_renderer::graphics; @@ -19,6 +12,7 @@ mod column; mod mouse_area; mod row; mod space; +mod stack; mod themer; pub mod button; @@ -85,6 +79,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use space::Space; #[doc(no_inline)] +pub use stack::Stack; +#[doc(no_inline)] pub use text::Text; #[doc(no_inline)] pub use text_editor::TextEditor; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 9634e477..d7235cf6 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -232,7 +232,7 @@ where ); match (self.interaction, content_interaction) { - (Some(interaction), mouse::Interaction::Idle) + (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => { interaction diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 0364f980..98efe305 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -1,5 +1,4 @@ //! Build and show dropdown menus. -use crate::container::{self, Container}; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; @@ -20,12 +19,15 @@ use crate::scrollable::{self, Scrollable}; #[allow(missing_debug_implementations)] pub struct Menu< 'a, + 'b, T, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where + Theme: Catalog, Renderer: text::Renderer, + 'b: 'a, { state: &'a mut State, options: &'a [T], @@ -38,15 +40,17 @@ pub struct Menu< text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: &'a Style<'a, Theme>, + class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer> +impl<'a, 'b, T, Message, Theme, Renderer> + Menu<'a, 'b, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, + 'b: 'a, { /// Creates a new [`Menu`] with the given [`State`], a list of options, /// the message to produced when an option is selected, and its [`Style`]. @@ -56,7 +60,7 @@ where hovered_option: &'a mut Option<usize>, on_selected: impl FnMut(T) -> Message + 'a, on_option_hovered: Option<&'a dyn Fn(T) -> Message>, - style: &'a Style<'a, Theme>, + class: &'a <Theme as Catalog>::Class<'b>, ) -> Self { Menu { state, @@ -70,7 +74,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style, + class, } } @@ -153,27 +157,29 @@ impl Default for State { } } -struct Overlay<'a, Message, Theme, Renderer> +struct Overlay<'a, 'b, Message, Theme, Renderer> where + Theme: Catalog, Renderer: crate::core::Renderer, { position: Point, state: &'a mut Tree, - container: Container<'a, Message, Theme, Renderer>, + list: Scrollable<'a, Message, Theme, Renderer>, width: f32, target_height: f32, - style: &'a Style<'a, Theme>, + class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> +impl<'a, 'b, Message, Theme, Renderer> Overlay<'a, 'b, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, + Theme: Catalog + scrollable::Catalog + 'a, Renderer: text::Renderer + 'a, + 'b: 'a, { pub fn new<T>( position: Point, - menu: Menu<'a, T, Message, Theme, Renderer>, + menu: Menu<'a, 'b, T, Message, Theme, Renderer>, target_height: f32, ) -> Self where @@ -191,46 +197,43 @@ where text_size, text_line_height, text_shaping, - style, + class, } = menu; - let container = Container::with_style( - Scrollable::with_direction_and_style( - List { - options, - hovered_option, - on_selected, - on_option_hovered, - font, - text_size, - text_line_height, - text_shaping, - padding, - style: &style.list, - }, - scrollable::Direction::default(), - &style.scrollable, - ), - container::transparent, + let list = Scrollable::with_direction( + List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + class, + }, + scrollable::Direction::default(), ); - state.tree.diff(&container as &dyn Widget<_, _, _>); + state.tree.diff(&list as &dyn Widget<_, _, _>); Self { position, state: &mut state.tree, - container, + list, width, target_height, - style, + class, } } } -impl<'a, Message, Theme, Renderer> +impl<'a, 'b, Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer> - for Overlay<'a, Message, Theme, Renderer> + for Overlay<'a, 'b, Message, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -251,7 +254,7 @@ where ) .width(self.width); - let node = self.container.layout(self.state, renderer, &limits); + let node = self.list.layout(self.state, renderer, &limits); let size = node.size(); node.move_to(if space_below > space_above { @@ -272,7 +275,7 @@ where ) -> event::Status { let bounds = layout.bounds(); - self.container.on_event( + self.list.on_event( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) @@ -285,7 +288,7 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.container + self.list .mouse_interaction(self.state, layout, cursor, viewport, renderer) } @@ -293,30 +296,32 @@ where &self, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, ) { let bounds = layout.bounds(); - let appearance = (self.style.list)(theme); + let style = Catalog::style(theme, self.class); renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); - self.container - .draw(self.state, renderer, theme, style, layout, cursor, &bounds); + self.list.draw( + self.state, renderer, theme, defaults, layout, cursor, &bounds, + ); } } -struct List<'a, T, Message, Theme, Renderer> +struct List<'a, 'b, T, Message, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { options: &'a [T], @@ -328,13 +333,14 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: &'a dyn Fn(&Theme) -> Appearance, + class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for List<'a, T, Message, Theme, Renderer> +impl<'a, 'b, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for List<'a, 'b, T, Message, Theme, Renderer> where T: Clone + ToString, + Theme: Catalog, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -477,7 +483,7 @@ where _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let appearance = (self.style)(theme); + let style = Catalog::style(theme, self.class); let bounds = layout.bounds(); let text_size = @@ -507,20 +513,20 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + appearance.border.width, - width: bounds.width - appearance.border.width * 2.0, + x: bounds.x + style.border.width, + width: bounds.width - style.border.width * 2.0, ..bounds }, - border: Border::rounded(appearance.border.radius), + border: Border::rounded(style.border.radius), ..renderer::Quad::default() }, - appearance.selected_background, + style.selected_background, ); } renderer.fill_text( Text { - content: &option.to_string(), + content: option.to_string(), bounds: Size::new(f32::INFINITY, bounds.height), size: text_size, line_height: self.text_line_height, @@ -531,9 +537,9 @@ where }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { - appearance.selected_text_color + style.selected_text_color } else { - appearance.text_color + style.text_color }, *viewport, ); @@ -541,23 +547,24 @@ where } } -impl<'a, T, Message, Theme, Renderer> - From<List<'a, T, Message, Theme, Renderer>> +impl<'a, 'b, T, Message, Theme, Renderer> + From<List<'a, 'b, T, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: 'a, + Theme: 'a + Catalog, Renderer: 'a + text::Renderer, + 'b: 'a, { - fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self { + fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self { Element::new(list) } } /// The appearance of a [`Menu`]. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the menu. pub background: Background, /// The [`Border`] of the menu. @@ -570,35 +577,43 @@ pub struct Appearance { pub selected_background: Background, } -/// The style of the different parts of a [`Menu`]. -#[allow(missing_debug_implementations)] -pub struct Style<'a, Theme> { - /// The style of the list of the [`Menu`]. - pub list: Box<dyn Fn(&Theme) -> Appearance + 'a>, - /// The style of the [`Scrollable`] of the [`Menu`]. - pub scrollable: scrollable::Style<'a, Theme>, -} +/// The theme catalog of a [`Menu`]. +pub trait Catalog: scrollable::Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; -/// The default style of a [`Menu`]. -pub trait DefaultStyle: Sized { - /// Returns the default style of a [`Menu`]. - fn default_style() -> Style<'static, Self>; + /// The default class for the scrollable of the [`Menu`]. + fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> { + <Self as scrollable::Catalog>::default() + } + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style() -> Style<'static, Self> { - Style { - list: Box::new(default), - scrollable: Box::new(scrollable::default), - } +/// A styling function for a [`Menu`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) + } + + fn style(&self, class: &StyleFn<'_, Self>) -> Style { + class(self) } } /// The default style of the list of a [`Menu`]. -pub fn default(theme: &Theme) -> Appearance { +pub fn default(theme: &Theme) -> Style { let palette = theme.extended_palette(); - Appearance { + Style { background: palette.background.weak.color.into(), border: Border { width: 1.0, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index beac0bd8..acfa9d44 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -30,6 +30,7 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; +use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -39,8 +40,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, Background, Border, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; const DRAG_DEADBAND_DISTANCE: f32 = 10.0; @@ -101,7 +102,8 @@ pub struct PaneGrid< Theme = crate::Theme, Renderer = crate::Renderer, > where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { contents: Contents<'a, Content<'a, Message, Theme, Renderer>>, width: Length, @@ -110,12 +112,13 @@ pub struct PaneGrid< on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - style: Style<'a, Theme>, + class: <Theme as Catalog>::Class<'a>, } impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// @@ -124,10 +127,7 @@ where pub fn new<T>( state: &'a State<T>, view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>, - ) -> Self - where - Theme: DefaultStyle + 'a, - { + ) -> Self { let contents = if let Some((pane, pane_state)) = state.maximized.and_then(|pane| { state.panes.get(&pane).map(|pane_state| (pane, pane_state)) @@ -158,7 +158,7 @@ where on_click: None, on_drag: None, on_resize: None, - style: Box::new(Theme::default_style), + class: <Theme as Catalog>::default(), } } @@ -218,8 +218,23 @@ where } /// Sets the style of the [`PaneGrid`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`PaneGrid`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class( + mut self, + class: impl Into<<Theme as Catalog>::Class<'a>>, + ) -> Self { + self.class = class.into(); self } @@ -233,7 +248,8 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for PaneGrid<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<state::Action>() @@ -596,7 +612,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -677,7 +693,7 @@ where None }; - let appearance = (self.style)(theme); + let style = Catalog::style(theme, &self.class); for ((id, (content, tree)), pane_layout) in contents.zip(layout.children()) @@ -692,7 +708,7 @@ where tree, renderer, theme, - style, + defaults, pane_layout, pane_cursor, viewport, @@ -710,10 +726,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: appearance.hovered_region.border, + border: style.hovered_region.border, ..renderer::Quad::default() }, - appearance.hovered_region.background, + style.hovered_region.background, ); } } @@ -723,7 +739,7 @@ where tree, renderer, theme, - style, + defaults, pane_layout, pane_cursor, viewport, @@ -738,10 +754,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: appearance.hovered_region.border, + border: style.hovered_region.border, ..renderer::Quad::default() }, - appearance.hovered_region.background, + style.hovered_region.background, ); } @@ -759,7 +775,7 @@ where tree, renderer, theme, - style, + defaults, layout, pane_cursor, viewport, @@ -772,9 +788,9 @@ where if picked_pane.is_none() { if let Some((axis, split_region, is_picked)) = picked_split { let highlight = if is_picked { - appearance.picked_split + style.picked_split } else { - appearance.hovered_split + style.hovered_split }; renderer.fill_quad( @@ -832,8 +848,8 @@ impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, - Renderer: crate::core::Renderer + 'a, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( pane_grid: PaneGrid<'a, Message, Theme, Renderer>, @@ -1116,7 +1132,7 @@ impl<'a, T> Contents<'a, T> { /// The appearance of a [`PaneGrid`]. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { +pub struct Style { /// The appearance of a hovered region highlight. pub hovered_region: Highlight, /// The appearance of a picked split. @@ -1145,32 +1161,40 @@ pub struct Line { pub width: f32, } -/// The style of a [`PaneGrid`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`PaneGrid`]. +pub trait Catalog: container::Catalog { + /// The item class of this [`Catalog`]. + type Class<'a>; -/// The default style of a [`PaneGrid`]. -pub trait DefaultStyle { - /// Returns the default style of a [`PaneGrid`]. - fn default_style(&self) -> Appearance; + /// The default class produced by this [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) +/// A styling function for a [`PaneGrid`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self) -> Appearance { - *self + fn style(&self, class: &StyleFn<'_, Self>) -> Style { + class(self) } } /// The default style of a [`PaneGrid`]. -pub fn default(theme: &Theme) -> Appearance { +pub fn default(theme: &Theme) -> Style { let palette = theme.extended_palette(); - Appearance { + Style { hovered_region: Highlight { background: Background::Color(Color { a: 0.5, diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 98f4f99a..30ad52ca 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -6,7 +6,7 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, }; use crate::pane_grid::{Draggable, TitleBar}; @@ -20,30 +20,29 @@ pub struct Content< Theme = crate::Theme, Renderer = crate::Renderer, > where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>, body: Element<'a, Message, Theme, Renderer>, - style: container::Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { /// Creates a new [`Content`] with the provided body. - pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self - where - Theme: container::DefaultStyle + 'a, - { + pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self { Self { title_bar: None, body: body.into(), - style: Box::new(Theme::default_style), + class: Theme::default(), } } - /// Sets the [`TitleBar`] of this [`Content`]. + /// Sets the [`TitleBar`] of the [`Content`]. pub fn title_bar( mut self, title_bar: TitleBar<'a, Message, Theme, Renderer>, @@ -53,18 +52,31 @@ where } /// Sets the style of the [`Content`]. + #[must_use] pub fn style( mut self, - style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + style: impl Fn(&Theme) -> container::Style + 'a, + ) -> Self + where + Theme::Class<'a>: From<container::StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Content`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { let children = if let Some(title_bar) = self.title_bar.as_ref() { @@ -93,7 +105,7 @@ where /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::core::Renderer + /// [`Renderer`]: core::Renderer pub fn draw( &self, tree: &Tree, @@ -107,15 +119,7 @@ where let bounds = layout.bounds(); { - let style = { - let status = if cursor.is_over(bounds) { - container::Status::Hovered - } else { - container::Status::Idle - }; - - (self.style)(theme, status) - }; + let style = theme.style(&self.class); container::draw_background(renderer, &style, bounds); } @@ -381,7 +385,8 @@ where impl<'a, Message, Theme, Renderer> Draggable for &Content<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { fn can_be_dragged_at( &self, @@ -403,8 +408,8 @@ impl<'a, T, Message, Theme, Renderer> From<T> for Content<'a, Message, Theme, Renderer> where T: Into<Element<'a, Message, Theme, Renderer>>, - Theme: container::DefaultStyle + 'a, - Renderer: crate::core::Renderer, + Theme: container::Catalog + 'a, + Renderer: core::Renderer, { fn from(element: T) -> Self { Self::new(element) diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 481cd770..c20c3b9c 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -6,7 +6,7 @@ use crate::pane_grid::{ Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, }; -use std::collections::HashMap; +use rustc_hash::FxHashMap; /// The state of a [`PaneGrid`]. /// @@ -25,7 +25,7 @@ pub struct State<T> { /// The panes of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid - pub panes: HashMap<Pane, T>, + pub panes: FxHashMap<Pane, T>, /// The internal state of the [`PaneGrid`]. /// @@ -52,7 +52,7 @@ impl<T> State<T> { /// Creates a new [`State`] with the given [`Configuration`]. pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { - let mut panes = HashMap::new(); + let mut panes = FxHashMap::default(); let internal = Internal::from_configuration(&mut panes, config.into(), 0); @@ -353,7 +353,7 @@ impl Internal { /// /// [`PaneGrid`]: super::PaneGrid pub fn from_configuration<T>( - panes: &mut HashMap<Pane, T>, + panes: &mut FxHashMap<Pane, T>, content: Configuration<T>, next_id: usize, ) -> Self { diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 8dfea6e3..c2eeebb7 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -6,7 +6,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, + Vector, }; /// The title bar of a [`Pane`]. @@ -19,32 +20,31 @@ pub struct TitleBar< Theme = crate::Theme, Renderer = crate::Renderer, > where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { content: Element<'a, Message, Theme, Renderer>, controls: Option<Element<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, - style: container::Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { /// Creates a new [`TitleBar`] with the given content. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self - where - Theme: container::DefaultStyle + 'a, - { + ) -> Self { Self { content: content.into(), controls: None, padding: Padding::ZERO, always_show_controls: false, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -63,15 +63,6 @@ where self } - /// Sets the style of the [`TitleBar`]. - pub fn style( - mut self, - style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a, - ) -> Self { - self.style = Box::new(style); - self - } - /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are /// always visible. /// @@ -84,11 +75,33 @@ where self.always_show_controls = true; self } + + /// Sets the style of the [`TitleBar`]. + #[must_use] + pub fn style( + mut self, + style: impl Fn(&Theme) -> container::Style + 'a, + ) -> Self + where + Theme::Class<'a>: From<container::StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`TitleBar`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { let children = if let Some(controls) = self.controls.as_ref() { @@ -117,7 +130,7 @@ where /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::core::Renderer + /// [`Renderer`]: core::Renderer pub fn draw( &self, tree: &Tree, @@ -130,16 +143,7 @@ where show_controls: bool, ) { let bounds = layout.bounds(); - - let style = { - let status = if cursor.is_over(bounds) { - container::Status::Hovered - } else { - container::Status::Idle - }; - - (self.style)(theme, status) - }; + let style = theme.style(&self.class); let inherited_style = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 52d54397..edccfdaa 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -32,6 +32,7 @@ pub struct PickList< T: ToString + PartialEq + Clone, L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, + Theme: Catalog, Renderer: text::Renderer, { on_select: Box<dyn Fn(T) -> Message + 'a>, @@ -47,7 +48,8 @@ pub struct PickList< text_shaping: text::Shaping, font: Option<Renderer::Font>, handle: Handle<Renderer::Font>, - style: Style<'a, Theme>, + class: <Theme as Catalog>::Class<'a>, + menu_class: <Theme as menu::Catalog>::Class<'a>, } impl<'a, T, L, V, Message, Theme, Renderer> @@ -57,6 +59,7 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { /// Creates a new [`PickList`] with the given list of options, the current @@ -65,10 +68,7 @@ where options: L, selected: Option<V>, on_select: impl Fn(T) -> Message + 'a, - ) -> Self - where - Theme: DefaultStyle, - { + ) -> Self { Self { on_select: Box::new(on_select), on_open: None, @@ -83,7 +83,8 @@ where text_shaping: text::Shaping::Basic, font: None, handle: Handle::default(), - style: Theme::default_style(), + class: <Theme as Catalog>::default(), + menu_class: <Theme as Catalog>::default_menu(), } } @@ -151,8 +152,23 @@ where } /// Sets the style of the [`PickList`]. - pub fn style(mut self, style: impl Into<Style<'a, Theme>>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`PickList`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class( + mut self, + class: impl Into<<Theme as Catalog>::Class<'a>>, + ) -> Self { + self.class = class.into(); self } } @@ -164,6 +180,7 @@ where L: Borrow<[T]>, V: Borrow<T>, Message: Clone + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn tag(&self) -> tree::Tag { @@ -409,15 +426,15 @@ where Status::Active }; - let appearance = (self.style.field)(theme, status); + let style = Catalog::style(theme, &self.class, status); renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); let handle = match &self.handle { @@ -462,7 +479,7 @@ where renderer.fill_text( Text { - content: &code_point.to_string(), + content: code_point.to_string(), size, line_height, font, @@ -478,14 +495,14 @@ where bounds.x + bounds.width - self.padding.right, bounds.center_y(), ), - appearance.handle_color, + style.handle_color, *viewport, ); } let label = selected.map(ToString::to_string); - if let Some(label) = label.as_deref().or(self.placeholder.as_deref()) { + if let Some(label) = label.or_else(|| self.placeholder.clone()) { let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); @@ -505,9 +522,9 @@ where }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { - appearance.text_color + style.text_color } else { - appearance.placeholder_color + style.placeholder_color }, *viewport, ); @@ -539,7 +556,7 @@ where (on_select)(option) }, None, - &self.style.menu, + &self.menu_class, ) .width(bounds.width) .padding(self.padding) @@ -565,7 +582,7 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone + 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -662,7 +679,7 @@ pub enum Status { /// The appearance of a pick list. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The text [`Color`] of the pick list. pub text_color: Color, /// The placeholder [`Color`] of the pick list. @@ -675,36 +692,49 @@ pub struct Appearance { pub border: Border, } -/// The styles of the different parts of a [`PickList`]. -#[allow(missing_debug_implementations)] -pub struct Style<'a, Theme> { - /// The style of the [`PickList`] itself. - pub field: Box<dyn Fn(&Theme, Status) -> Appearance + 'a>, +/// The theme catalog of a [`PickList`]. +pub trait Catalog: menu::Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; - /// The style of the [`Menu`] of the pick list. - pub menu: menu::Style<'a, Theme>, -} + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; -/// The default style of a [`PickList`]. -pub trait DefaultStyle: Sized { - /// Returns the default style of a [`PickList`]. - fn default_style() -> Style<'static, Self>; + /// The default class for the menu of the [`PickList`]. + fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> { + <Self as menu::Catalog>::default() + } + + /// The [`Style`] of a class with the given status. + fn style( + &self, + class: &<Self as Catalog>::Class<'_>, + status: Status, + ) -> Style; } -impl DefaultStyle for Theme { - fn default_style() -> Style<'static, Self> { - Style { - field: Box::new(default), - menu: menu::DefaultStyle::default_style(), - } +/// A styling function for a [`PickList`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) + } + + fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style { + class(self, status) } } /// The default style of the field of a [`PickList`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let active = Appearance { + let active = Style { text_color: palette.background.weak.text, background: palette.background.weak.color.into(), placeholder_color: palette.background.strong.color, @@ -718,7 +748,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { match status { Status::Active => active, - Status::Hovered | Status::Opened => Appearance { + Status::Hovered | Status::Opened => Style { border: Border { color: palette.primary.strong.color, ..active.border diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 38d8da85..e7821b43 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -4,7 +4,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - Background, Border, Element, Layout, Length, Rectangle, Size, Theme, Widget, + self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme, + Widget, }; use std::ops::RangeInclusive; @@ -22,15 +23,21 @@ use std::ops::RangeInclusive; /// ///  #[allow(missing_debug_implementations)] -pub struct ProgressBar<'a, Theme = crate::Theme> { +pub struct ProgressBar<'a, Theme = crate::Theme> +where + Theme: Catalog, +{ range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } -impl<'a, Theme> ProgressBar<'a, Theme> { +impl<'a, Theme> ProgressBar<'a, Theme> +where + Theme: Catalog, +{ /// The default height of a [`ProgressBar`]. pub const DEFAULT_HEIGHT: f32 = 30.0; @@ -39,16 +46,13 @@ impl<'a, Theme> ProgressBar<'a, Theme> { /// It expects: /// * an inclusive range of possible values /// * the current value of the [`ProgressBar`] - pub fn new(range: RangeInclusive<f32>, value: f32) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { ProgressBar { value: value.clamp(*range.start(), *range.end()), range, width: Length::Fill, height: None, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -65,8 +69,20 @@ impl<'a, Theme> ProgressBar<'a, Theme> { } /// Sets the style of the [`ProgressBar`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`ProgressBar`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -74,7 +90,8 @@ impl<'a, Theme> ProgressBar<'a, Theme> { impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for ProgressBar<'a, Theme> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn size(&self) -> Size<Length> { Size { @@ -116,15 +133,15 @@ where / (range_end - range_start) }; - let appearance = (self.style)(theme); + let style = theme.style(&self.class); renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); if active_progress_width > 0.0 { @@ -134,10 +151,10 @@ where width: active_progress_width, ..bounds }, - border: Border::rounded(appearance.border.radius), + border: Border::rounded(style.border.radius), ..renderer::Quad::default() }, - appearance.bar, + style.bar, ); } } @@ -147,8 +164,8 @@ impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + core::Renderer, { fn from( progress_bar: ProgressBar<'a, Theme>, @@ -159,7 +176,7 @@ where /// The appearance of a progress bar. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the progress bar. pub background: Background, /// The [`Background`] of the bar of the progress bar. @@ -168,29 +185,37 @@ pub struct Appearance { pub border: Border, } -/// The style of a [`ProgressBar`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`ProgressBar`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; -/// The default style of a [`ProgressBar`]. -pub trait DefaultStyle { - /// Returns the default style of a [`ProgressBar`]. - fn default_style(&self) -> Appearance; + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - primary(self) +/// A styling function for a [`ProgressBar`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) } -} -impl DefaultStyle for Appearance { - fn default_style(&self) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } /// The primary style of a [`ProgressBar`]. -pub fn primary(theme: &Theme) -> Appearance { +pub fn primary(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled( @@ -200,7 +225,7 @@ pub fn primary(theme: &Theme) -> Appearance { } /// The secondary style of a [`ProgressBar`]. -pub fn secondary(theme: &Theme) -> Appearance { +pub fn secondary(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled( @@ -210,14 +235,14 @@ pub fn secondary(theme: &Theme) -> Appearance { } /// The success style of a [`ProgressBar`]. -pub fn success(theme: &Theme) -> Appearance { +pub fn success(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled(palette.background.strong.color, palette.success.base.color) } /// The danger style of a [`ProgressBar`]. -pub fn danger(theme: &Theme) -> Appearance { +pub fn danger(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled(palette.background.strong.color, palette.danger.base.color) @@ -226,8 +251,8 @@ pub fn danger(theme: &Theme) -> Appearance { fn styled( background: impl Into<Background>, bar: impl Into<Background>, -) -> Appearance { - Appearance { +) -> Style { + Style { background: background.into(), bar: bar.into(), border: Border::rounded(2), diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 90c0c970..e064aada 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -8,7 +8,6 @@ use crate::core::{ Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector, Widget, }; -use crate::graphics::geometry::Renderer as _; use crate::Renderer; use std::cell::RefCell; @@ -20,22 +19,25 @@ const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which /// can be read by an imaging device, such as a camera. #[allow(missing_debug_implementations)] -pub struct QRCode<'a, Theme = crate::Theme> { +pub struct QRCode<'a, Theme = crate::Theme> +where + Theme: Catalog, +{ data: &'a Data, cell_size: u16, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } -impl<'a, Theme> QRCode<'a, Theme> { +impl<'a, Theme> QRCode<'a, Theme> +where + Theme: Catalog, +{ /// Creates a new [`QRCode`] with the provided [`Data`]. - pub fn new(data: &'a Data) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(data: &'a Data) -> Self { Self { data, cell_size: DEFAULT_CELL_SIZE, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -46,14 +48,27 @@ impl<'a, Theme> QRCode<'a, Theme> { } /// Sets the style of the [`QRCode`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`QRCode`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } -impl<'a, Message, Theme> Widget<Message, Theme, Renderer> - for QRCode<'a, Theme> +impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> +where + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -97,13 +112,13 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> let bounds = layout.bounds(); let side_length = self.data.width + 2 * QUIET_ZONE; - let appearance = (self.style)(theme); - let mut last_appearance = state.last_appearance.borrow_mut(); + let style = theme.style(&self.class); + let mut last_style = state.last_style.borrow_mut(); - if Some(appearance) != *last_appearance { + if Some(style) != *last_style { self.data.cache.clear(); - *last_appearance = Some(appearance); + *last_style = Some(style); } // Reuse cache if possible @@ -115,7 +130,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> frame.fill_rectangle( Point::ORIGIN, Size::new(side_length as f32, side_length as f32), - appearance.background, + style.background, ); // Avoid drawing on the quiet zone @@ -134,7 +149,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> frame.fill_rectangle( Point::new(column as f32, row as f32), Size::UNIT, - appearance.cell, + style.cell, ); }); }); @@ -142,7 +157,9 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> renderer.with_translation( bounds.position() - Point::ORIGIN, |renderer| { - renderer.draw(vec![geometry]); + use crate::graphics::geometry::Renderer as _; + + renderer.draw_geometry(geometry); }, ); } @@ -151,7 +168,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> impl<'a, Message, Theme> From<QRCode<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: 'a, + Theme: Catalog + 'a, { fn from(qr_code: QRCode<'a, Theme>) -> Self { Self::new(qr_code) @@ -165,7 +182,7 @@ where pub struct Data { contents: Vec<qrcode::Color>, width: usize, - cache: canvas::Cache, + cache: canvas::Cache<Renderer>, } impl Data { @@ -323,44 +340,50 @@ impl From<qrcode::types::QrError> for Error { #[derive(Default)] struct State { - last_appearance: RefCell<Option<Appearance>>, + last_style: RefCell<Option<Style>>, } /// The appearance of a QR code. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { +pub struct Style { /// The color of the QR code data cells pub cell: Color, /// The color of the QR code background pub background: Color, } -/// The style of a [`QRCode`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`QRCode`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`QRCode`]. -pub trait DefaultStyle { - /// Returns the default style of a [`QRCode`]. - fn default_style(&self) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) +/// A styling function for a [`QRCode`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } /// The default style of a [`QRCode`]. -pub fn default(theme: &Theme) -> Appearance { +pub fn default(theme: &Theme) -> Style { let palette = theme.palette(); - Appearance { + Style { cell: palette.text, background: palette.background, } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index a7b7dd03..6b22961d 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -69,6 +69,7 @@ use crate::core::{ #[allow(missing_debug_implementations)] pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where + Theme: Catalog, Renderer: text::Renderer, { is_selected: bool, @@ -81,12 +82,13 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> where Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { /// The default size of a [`Radio`] button. @@ -110,7 +112,6 @@ where f: F, ) -> Self where - Theme: DefaultStyle + 'a, V: Eq + Copy, F: FnOnce(V) -> Message, { @@ -125,7 +126,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -175,11 +176,20 @@ where } /// Sets the style of the [`Radio`] button. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Radio`] button. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -188,6 +198,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Radio<'a, Message, Theme, Renderer> where Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -284,7 +295,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -300,7 +311,7 @@ where Status::Active { is_selected } }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); { let layout = children.next().unwrap(); @@ -314,12 +325,12 @@ where bounds, border: Border { radius: (size / 2.0).into(), - width: appearance.border_width, - color: appearance.border_color, + width: style.border_width, + color: style.border_color, }, ..renderer::Quad::default() }, - appearance.background, + style.background, ); if self.is_selected { @@ -334,7 +345,7 @@ where border: Border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, - appearance.dot_color, + style.dot_color, ); } } @@ -344,11 +355,11 @@ where crate::text::draw( renderer, - style, + defaults, label_layout, tree.state.downcast_ref(), - crate::text::Appearance { - color: appearance.text_color, + crate::text::Style { + color: style.text_color, }, viewport, ); @@ -360,7 +371,7 @@ impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: 'a, + Theme: 'a + Catalog, Renderer: 'a + text::Renderer, { fn from( @@ -387,7 +398,7 @@ pub enum Status { /// The appearance of a radio button. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the radio button. pub background: Background, /// The [`Color`] of the dot of the radio button. @@ -400,32 +411,38 @@ pub struct Appearance { pub text_color: Option<Color>, } -/// The style of a [`Radio`] button. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Radio`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Radio`] button. -pub trait DefaultStyle { - /// Returns the default style of a [`Radio`] button. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`Radio`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`Radio`] button. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let active = Appearance { + let active = Style { background: Color::TRANSPARENT.into(), dot_color: palette.primary.strong.color, border_width: 1.0, @@ -435,7 +452,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { match status { Status::Active { .. } => active, - Status::Hovered { .. } => Appearance { + Status::Hovered { .. } => Style { dot_color: palette.primary.strong.color, background: palette.primary.weak.color.into(), ..active diff --git a/widget/src/row.rs b/widget/src/row.rs index 47feff9c..fa352171 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -31,11 +31,18 @@ where Self::from_vec(Vec::new()) } + /// Creates a [`Row`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + /// Creates a [`Row`] with the given elements. pub fn with_children( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, ) -> Self { - Self::new().extend(children) + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) } /// Creates a [`Row`] from an already allocated [`Vec`]. diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 9fa5f74f..1a536d2f 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,4 +1,5 @@ //! Display a horizontal or vertical rule for dividing content. +use crate::core; use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; @@ -10,43 +11,55 @@ use crate::core::{ /// Display a horizontal or vertical rule for dividing content. #[allow(missing_debug_implementations)] -pub struct Rule<'a, Theme = crate::Theme> { +pub struct Rule<'a, Theme = crate::Theme> +where + Theme: Catalog, +{ width: Length, height: Length, is_horizontal: bool, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } -impl<'a, Theme> Rule<'a, Theme> { +impl<'a, Theme> Rule<'a, Theme> +where + Theme: Catalog, +{ /// Creates a horizontal [`Rule`] with the given height. - pub fn horizontal(height: impl Into<Pixels>) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn horizontal(height: impl Into<Pixels>) -> Self { Rule { width: Length::Fill, height: Length::Fixed(height.into().0), is_horizontal: true, - style: Box::new(Theme::default_style), + class: Theme::default(), } } /// Creates a vertical [`Rule`] with the given width. - pub fn vertical(width: impl Into<Pixels>) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn vertical(width: impl Into<Pixels>) -> Self { Rule { width: Length::Fixed(width.into().0), height: Length::Fill, is_horizontal: false, - style: Box::new(Theme::default_style), + class: Theme::default(), } } /// Sets the style of the [`Rule`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Rule`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -54,7 +67,8 @@ impl<'a, Theme> Rule<'a, Theme> { impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<'a, Theme> where - Renderer: crate::core::Renderer, + Renderer: core::Renderer, + Theme: Catalog, { fn size(&self) -> Size<Length> { Size { @@ -83,35 +97,34 @@ where _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let appearance = (self.style)(theme); + let style = theme.style(&self.class); let bounds = if self.is_horizontal { let line_y = (bounds.y + (bounds.height / 2.0) - - (appearance.width as f32 / 2.0)) + - (style.width as f32 / 2.0)) .round(); - let (offset, line_width) = appearance.fill_mode.fill(bounds.width); + let (offset, line_width) = style.fill_mode.fill(bounds.width); let line_x = bounds.x + offset; Rectangle { x: line_x, y: line_y, width: line_width, - height: appearance.width as f32, + height: style.width as f32, } } else { let line_x = (bounds.x + (bounds.width / 2.0) - - (appearance.width as f32 / 2.0)) + - (style.width as f32 / 2.0)) .round(); - let (offset, line_height) = - appearance.fill_mode.fill(bounds.height); + let (offset, line_height) = style.fill_mode.fill(bounds.height); let line_y = bounds.y + offset; Rectangle { x: line_x, y: line_y, - width: appearance.width as f32, + width: style.width as f32, height: line_height, } }; @@ -119,10 +132,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::rounded(appearance.radius), + border: Border::rounded(style.radius), ..renderer::Quad::default() }, - appearance.color, + style.color, ); } } @@ -131,8 +144,8 @@ impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + core::Renderer, { fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { Element::new(rule) @@ -141,7 +154,7 @@ where /// The appearance of a rule. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The color of the rule. pub color: Color, /// The width (thickness) of the rule line. @@ -216,32 +229,40 @@ impl FillMode { } } -/// The style of a [`Rule`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`Rule`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Rule`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Rule`]. - fn default_style(&self) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) +/// A styling function for a [`Rule`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } /// The default styling of a [`Rule`]. -pub fn default(theme: &Theme) -> Appearance { +pub fn default(theme: &Theme) -> Style { let palette = theme.extended_palette(); - Appearance { + Style { color: palette.background.strong.color, width: 1, radius: 0.0.into(), diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c03bbb7d..6fc00f87 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -12,8 +12,8 @@ use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, Background, Border, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; @@ -28,7 +28,8 @@ pub struct Scrollable< Theme = crate::Theme, Renderer = crate::Renderer, > where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { id: Option<Id>, width: Length, @@ -36,20 +37,18 @@ pub struct Scrollable< direction: Direction, content: Element<'a, Message, Theme, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { /// Creates a new vertical [`Scrollable`]. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self - where - Theme: DefaultStyle + 'a, - { + ) -> Self { Self::with_direction(content, Direction::default()) } @@ -57,18 +56,6 @@ where pub fn with_direction( content: impl Into<Element<'a, Message, Theme, Renderer>>, direction: Direction, - ) -> Self - where - Theme: DefaultStyle + 'a, - { - Self::with_direction_and_style(content, direction, Theme::default_style) - } - - /// Creates a new [`Scrollable`] with the given [`Direction`] and style. - pub fn with_direction_and_style( - content: impl Into<Element<'a, Message, Theme, Renderer>>, - direction: Direction, - style: impl Fn(&Theme, Status) -> Appearance + 'a, ) -> Self { let content = content.into(); @@ -91,7 +78,7 @@ where direction, content, on_scroll: None, - style: Box::new(style), + class: Theme::default(), } } @@ -121,12 +108,21 @@ where self } - /// Sets the style of the [`Scrollable`] . - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + /// Sets the style of this [`Scrollable`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Scrollable`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -237,7 +233,8 @@ pub enum Alignment { impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Scrollable<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -353,6 +350,148 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } + let mut event_status = { let cursor = match cursor_over_scrollable { Some(cursor_position) @@ -425,7 +564,9 @@ where let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { + let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed + && state.keyboard_modifiers.shift() + { Vector::new(y, x) } else { Vector::new(x, y) @@ -438,15 +579,17 @@ where state.scroll(delta, self.direction, bounds, content_bounds); - notify_on_scroll( + event_status = if notify_on_scroll( state, &self.on_scroll, bounds, content_bounds, shell, - ); - - event_status = event::Status::Captured; + ) { + event::Status::Captured + } else { + event::Status::Ignored + }; } Event::Touch(event) if state.scroll_area_touched_at.is_some() @@ -484,7 +627,8 @@ where state.scroll_area_touched_at = Some(cursor_position); - notify_on_scroll( + // TODO: bubble up touch movements if not consumed. + let _ = notify_on_scroll( state, &self.on_scroll, bounds, @@ -501,148 +645,6 @@ where _ => {} } - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_y_scroller(cursor_position), - scrollbars.y, - ) { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_x_scroller(cursor_position), - scrollbars.x, - ) { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } - event_status } @@ -651,10 +653,10 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State>(); @@ -662,6 +664,10 @@ where let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); + let Some(visible_bounds) = bounds.intersection(viewport) else { + return; + }; + let scrollbars = Scrollbars::new(state, self.direction, bounds, content_bounds); @@ -701,17 +707,13 @@ where Status::Active }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); - container::draw_background( - renderer, - &appearance.container, - layout.bounds(), - ); + container::draw_background(renderer, &style.container, layout.bounds()); // Draw inner content if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { + renderer.with_layer(visible_bounds, |renderer| { renderer.with_translation( Vector::new(-translation.x, -translation.y), |renderer| { @@ -719,7 +721,7 @@ where &tree.children[0], renderer, theme, - style, + defaults, content_layout, cursor, &Rectangle { @@ -774,15 +776,15 @@ where renderer.with_layer( Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds + width: (visible_bounds.width + 2.0).min(viewport.width), + height: (visible_bounds.height + 2.0).min(viewport.height), + ..visible_bounds }, |renderer| { if let Some(scrollbar) = scrollbars.y { draw_scrollbar( renderer, - appearance.vertical_scrollbar, + style.vertical_scrollbar, &scrollbar, ); } @@ -790,14 +792,14 @@ where if let Some(scrollbar) = scrollbars.x { draw_scrollbar( renderer, - appearance.horizontal_scrollbar, + style.horizontal_scrollbar, &scrollbar, ); } if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) { let background = - appearance.gap.or(appearance.container.background); + style.gap.or(style.container.background); if let Some(background) = background { renderer.fill_quad( @@ -821,7 +823,7 @@ where &tree.children[0], renderer, theme, - style, + defaults, content_layout, cursor, &Rectangle { @@ -857,7 +859,7 @@ where if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() { - mouse::Interaction::Idle + mouse::Interaction::None } else { let translation = state.translation(self.direction, bounds, content_bounds); @@ -916,8 +918,8 @@ impl<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + core::Renderer, { fn from( text_input: Scrollable<'a, Message, Theme, Renderer>, @@ -968,51 +970,54 @@ pub fn scroll_to<Message: 'static>( Command::widget(operation::scrollable::scroll_to(id.0, offset)) } +/// Returns [`true`] if the viewport actually changed. fn notify_on_scroll<Message>( state: &mut State, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, -) { - if let Some(on_scroll) = on_scroll { - if content_bounds.width <= bounds.width - && content_bounds.height <= bounds.height - { - return; - } +) -> bool { + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return false; + } - let viewport = Viewport { - offset_x: state.offset_x, - offset_y: state.offset_y, - bounds, - content_bounds, - }; + let viewport = Viewport { + offset_x: state.offset_x, + offset_y: state.offset_y, + bounds, + content_bounds, + }; - // Don't publish redundant viewports to shell - if let Some(last_notified) = state.last_notified { - let last_relative_offset = last_notified.relative_offset(); - let current_relative_offset = viewport.relative_offset(); + // Don't publish redundant viewports to shell + if let Some(last_notified) = state.last_notified { + let last_relative_offset = last_notified.relative_offset(); + let current_relative_offset = viewport.relative_offset(); - let last_absolute_offset = last_notified.absolute_offset(); - let current_absolute_offset = viewport.absolute_offset(); + let last_absolute_offset = last_notified.absolute_offset(); + let current_absolute_offset = viewport.absolute_offset(); - let unchanged = |a: f32, b: f32| { - (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) - }; + let unchanged = |a: f32, b: f32| { + (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) + }; - if unchanged(last_relative_offset.x, current_relative_offset.x) - && unchanged(last_relative_offset.y, current_relative_offset.y) - && unchanged(last_absolute_offset.x, current_absolute_offset.x) - && unchanged(last_absolute_offset.y, current_absolute_offset.y) - { - return; - } + if unchanged(last_relative_offset.x, current_relative_offset.x) + && unchanged(last_relative_offset.y, current_relative_offset.y) + && unchanged(last_absolute_offset.x, current_absolute_offset.x) + && unchanged(last_absolute_offset.y, current_absolute_offset.y) + { + return false; } + } + if let Some(on_scroll) = on_scroll { shell.publish(on_scroll(viewport)); - state.last_notified = Some(viewport); } + state.last_notified = Some(viewport); + + true } #[derive(Debug, Clone, Copy)] @@ -1570,9 +1575,9 @@ pub enum Status { /// The appearance of a scrolable. #[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`container::Appearance`] of a scrollable. - pub container: container::Appearance, +pub struct Style { + /// The [`container::Style`] of a scrollable. + pub container: container::Style, /// The vertical [`Scrollbar`] appearance. pub vertical_scrollbar: Scrollbar, /// The horizontal [`Scrollbar`] appearance. @@ -1601,29 +1606,35 @@ pub struct Scroller { pub border: Border, } -/// The style of a [`Scrollable`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Scrollable`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Scrollable`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Scrollable`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`Scrollable`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`Scrollable`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let scrollbar = Scrollbar { @@ -1636,8 +1647,8 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { }; match status { - Status::Active => Appearance { - container: container::Appearance::default(), + Status::Active => Style { + container: container::Style::default(), vertical_scrollbar: scrollbar, horizontal_scrollbar: scrollbar, gap: None, @@ -1654,8 +1665,8 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { ..scrollbar }; - Appearance { - container: container::Appearance::default(), + Style { + container: container::Style::default(), vertical_scrollbar: if is_vertical_scrollbar_hovered { hovered_scrollbar } else { @@ -1681,8 +1692,8 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { ..scrollbar }; - Appearance { - container: container::Appearance::default(), + Style { + container: container::Style::default(), vertical_scrollbar: if is_vertical_scrollbar_dragged { dragged_scrollbar } else { diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 68112f83..fad2f4eb 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -13,12 +13,13 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; -use crate::renderer::wgpu::primitive::pipeline; +use crate::renderer::wgpu::primitive; use std::marker::PhantomData; +pub use crate::graphics::Viewport; pub use crate::renderer::wgpu::wgpu; -pub use pipeline::{Primitive, Storage}; +pub use primitive::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. /// @@ -60,7 +61,7 @@ impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Shader<Message, P> where P: Program<Message>, - Renderer: pipeline::Renderer, + Renderer: primitive::Renderer, { fn tag(&self) -> tree::Tag { struct Tag<T>(T); @@ -160,7 +161,7 @@ where let bounds = layout.bounds(); let state = tree.state.downcast_ref::<P::State>(); - renderer.draw_pipeline_primitive( + renderer.draw_primitive( bounds, self.program.draw(state, cursor_position, bounds), ); @@ -171,7 +172,7 @@ impl<'a, Message, Theme, Renderer, P> From<Shader<Message, P>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Renderer: pipeline::Renderer, + Renderer: primitive::Renderer, P: Program<Message> + 'a, { fn from( diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 6dd50404..902c7c3b 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -1,7 +1,7 @@ use crate::core::event; use crate::core::mouse; use crate::core::{Rectangle, Shell}; -use crate::renderer::wgpu::primitive::pipeline; +use crate::renderer::wgpu::Primitive; use crate::shader; /// The state and logic of a [`Shader`] widget. @@ -15,7 +15,7 @@ pub trait Program<Message> { type State: Default + 'static; /// The type of primitive this [`Program`] can draw. - type Primitive: pipeline::Primitive + 'static; + type Primitive: Primitive + 'static; /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a diff --git a/widget/src/slider.rs b/widget/src/slider.rs index d3b46a98..a8f1d192 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -9,7 +9,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Widget, }; @@ -39,7 +39,10 @@ use std::ops::RangeInclusive; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Theme = crate::Theme> { +pub struct Slider<'a, T, Message, Theme = crate::Theme> +where + Theme: Catalog, +{ range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -49,13 +52,14 @@ pub struct Slider<'a, T, Message, Theme = crate::Theme> { on_release: Option<Message>, width: Length, height: f32, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where T: Copy + From<u8> + PartialOrd, Message: Clone, + Theme: Catalog, { /// The default height of a [`Slider`]. pub const DEFAULT_HEIGHT: f32 = 16.0; @@ -70,7 +74,6 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where - Theme: DefaultStyle + 'a, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -95,7 +98,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -130,15 +133,6 @@ where self } - /// Sets the style of the [`Slider`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); - self - } - /// Sets the step size of the [`Slider`]. pub fn step(mut self, step: impl Into<T>) -> Self { self.step = step.into(); @@ -152,6 +146,24 @@ where self.shift_step = Some(shift_step.into()); self } + + /// Sets the style of the [`Slider`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Slider`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -159,7 +171,8 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -349,8 +362,8 @@ where let bounds = layout.bounds(); let is_mouse_over = cursor.is_over(bounds); - let style = (self.style)( - theme, + let style = theme.style( + &self.class, if state.is_dragging { Status::Dragged } else if is_mouse_over { @@ -461,8 +474,8 @@ impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>> where T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, Message: Clone + 'a, - Theme: 'a, - Renderer: crate::core::Renderer + 'a, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( slider: Slider<'a, T, Message, Theme>, @@ -490,15 +503,15 @@ pub enum Status { /// The appearance of a slider. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The colors of the rail of the slider. pub rail: Rail, /// The appearance of the [`Handle`] of the slider. pub handle: Handle, } -impl Appearance { - /// Changes the [`HandleShape`] of the [`Appearance`] to a circle +impl Style { + /// Changes the [`HandleShape`] of the [`Style`] to a circle /// with the given radius. pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self { self.handle.shape = HandleShape::Circle { @@ -549,29 +562,35 @@ pub enum HandleShape { }, } -/// The style of a [`Slider`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Slider`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Slider`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Slider`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`Slider`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`Slider`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let color = match status { @@ -580,7 +599,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { Status::Dragged => palette.primary.strong.color, }; - Appearance { + Style { rail: Rail { colors: (color, palette.secondary.base.color), width: 4.0, diff --git a/widget/src/stack.rs b/widget/src/stack.rs new file mode 100644 index 00000000..5035541b --- /dev/null +++ b/widget/src/stack.rs @@ -0,0 +1,333 @@ +//! Display content on top of other content. +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::{Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, +}; + +/// A container that displays children on top of each other. +/// +/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and +/// will be displayed as the base layer. Every consecutive [`Element`] will be +/// renderer on top; on its own layer. +/// +/// Keep in mind that too much layering will normally produce bad UX as well as +/// introduce certain rendering overhead. Use this widget sparingly! +#[allow(missing_debug_implementations)] +pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +{ + width: Length, + height: Length, + children: Vec<Element<'a, Message, Theme, Renderer>>, +} + +impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + /// Creates an empty [`Stack`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Stack`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Stack`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Stack`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Stack::width`] or [`Stack::height`] accordingly. + pub fn from_vec( + children: Vec<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + width: Length::Shrink, + height: Length::Shrink, + children, + } + } + + /// Sets the width of the [`Stack`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Stack`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Adds an element to the [`Stack`]. + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + let child = child.into(); + + if self.children.is_empty() { + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + } + + self.children.push(child); + self + } + + /// Adds an element to the [`Stack`], if `Some`. + pub fn push_maybe( + self, + child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Stack`] with the given children. + pub fn extend( + self, + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + if self.children.is_empty() { + return layout::Node::new(limits.resolve( + self.width, + self.height, + Size::ZERO, + )); + } + + let base = self.children[0].as_widget().layout( + &mut tree.children[0], + renderer, + &limits, + ); + + let size = limits.resolve(self.width, self.height, base.size()); + let limits = layout::Limits::new(Size::ZERO, size); + + let nodes = std::iter::once(base) + .chain(self.children[1..].iter().zip(&mut tree.children[1..]).map( + |(layer, tree)| { + let node = + layer.as_widget().layout(tree, renderer, &limits); + + node + }, + )) + .collect(); + + layout::Node::with_children(size, nodes) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .rev() + .zip(tree.children.iter_mut().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .find(|&status| status == event::Status::Captured) + .unwrap_or(event::Status::Ignored) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::None) + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for (i, ((layer, state), layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .enumerate() + { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + } + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self { + Self::new(stack) + } +} diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 1ac07ade..4551bcad 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector, - Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Theme, Vector, Widget, }; use std::path::PathBuf; @@ -20,36 +20,40 @@ pub use crate::core::svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg<'a, Theme = crate::Theme> { +pub struct Svg<'a, Theme = crate::Theme> +where + Theme: Catalog, +{ handle: Handle, width: Length, height: Length, content_fit: ContentFit, - style: Style<'a, Theme>, + class: Theme::Class<'a>, + rotation: Rotation, + opacity: f32, } -impl<'a, Theme> Svg<'a, Theme> { +impl<'a, Theme> Svg<'a, Theme> +where + Theme: Catalog, +{ /// Creates a new [`Svg`] from the given [`Handle`]. - pub fn new(handle: impl Into<Handle>) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(handle: impl Into<Handle>) -> Self { Svg { handle: handle.into(), width: Length::Fill, height: Length::Shrink, content_fit: ContentFit::Contain, - style: Box::new(Theme::default_style), + class: Theme::default(), + rotation: Rotation::default(), + opacity: 1.0, } } /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. #[must_use] - pub fn from_path(path: impl Into<PathBuf>) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn from_path(path: impl Into<PathBuf>) -> Self { Self::new(Handle::from_path(path)) } @@ -78,13 +82,36 @@ impl<'a, Theme> Svg<'a, Theme> { } } - /// Sets the style variant of this [`Svg`]. + /// Sets the style of the [`Svg`]. #[must_use] - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Svg`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } + + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Svg`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); self } } @@ -93,6 +120,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<'a, Theme> where Renderer: svg::Renderer, + Theme: Catalog, { fn size(&self) -> Size<Length> { Size { @@ -108,14 +136,17 @@ where limits: &layout::Limits, ) -> layout::Node { // The raw w/h of the underlying image - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + // The rotated size of the svg + let rotated_size = self.rotation.apply(image_size); + // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(self.width, self.height, image_size); + let raw_size = limits.resolve(self.width, self.height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); + let full_size = self.content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -142,37 +173,49 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = self.rotation.apply(image_size); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); - let is_mouse_over = cursor.is_over(bounds); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + let scale = Vector::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); + let final_size = image_size * scale; - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; - let status = if is_mouse_over { - Status::Hovered - } else { - Status::Idle - }; + let drawing_bounds = Rectangle::new(position, final_size); - let appearance = (self.style)(theme, status); + let is_mouse_over = cursor.is_over(bounds); - renderer.draw( + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; + + let style = theme.style(&self.class, status); + + let render = |renderer: &mut Renderer| { + renderer.draw_svg( self.handle.clone(), - appearance.color, - drawing_bounds + offset, + style.color, + drawing_bounds, + self.rotation.radians(), + self.opacity, ); }; @@ -189,7 +232,7 @@ where impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: 'a, + Theme: Catalog + 'a, Renderer: svg::Renderer + 'a, { fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { @@ -208,7 +251,7 @@ pub enum Status { /// The appearance of an [`Svg`]. #[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Appearance { +pub struct Style { /// The [`Color`] filter of an [`Svg`]. /// /// Useful for coloring a symbolic icon. @@ -217,23 +260,37 @@ pub struct Appearance { pub color: Option<Color>, } -/// The style of an [`Svg`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of an [`Svg`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of an [`Svg`]. -pub trait DefaultStyle { - /// Returns the default style of an [`Svg`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, _status: Status) -> Appearance { - Appearance::default() +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|_theme, _status| Style::default()) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self +/// A styling function for an [`Svg`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl<'a, Theme> From<Style> for StyleFn<'a, Theme> { + fn from(style: Style) -> Self { + Box::new(move |_theme, _status| style) } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 5b8f6a1b..7c0b98ea 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -32,6 +32,7 @@ pub struct TextEditor< Renderer = crate::Renderer, > where Highlighter: text::Highlighter, + Theme: Catalog, Renderer: text::Renderer, { content: &'a Content<Renderer>, @@ -41,7 +42,7 @@ pub struct TextEditor< width: Length, height: Length, padding: Padding, - style: Style<'a, Theme>, + class: Theme::Class<'a>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -53,13 +54,11 @@ pub struct TextEditor< impl<'a, Message, Theme, Renderer> TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { /// Creates new [`TextEditor`] with the given [`Content`]. - pub fn new(content: &'a Content<Renderer>) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(content: &'a Content<Renderer>) -> Self { Self { content, font: None, @@ -68,7 +67,7 @@ where width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), - style: Box::new(Theme::default_style), + class: Theme::default(), on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -82,6 +81,7 @@ impl<'a, Highlighter, Message, Theme, Renderer> TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, + Theme: Catalog, Renderer: text::Renderer, { /// Sets the height of the [`TextEditor`]. @@ -110,6 +110,21 @@ where self } + /// Sets the text size of the [`TextEditor`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.text_size = Some(size.into()); + self + } + + /// Sets the [`text::LineHeight`] of the [`TextEditor`]. + pub fn line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.line_height = line_height.into(); + self + } + /// Sets the [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into<Padding>) -> Self { self.padding = padding.into(); @@ -134,7 +149,7 @@ where width: self.width, height: self.height, padding: self.padding, - style: self.style, + class: self.class, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, @@ -142,11 +157,20 @@ where } /// Sets the style of the [`TextEditor`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`TextEditor`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -295,7 +319,9 @@ where } } -struct State<Highlighter: text::Highlighter> { +/// The state of a [`TextEditor`]. +#[derive(Debug)] +pub struct State<Highlighter: text::Highlighter> { is_focused: bool, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, @@ -305,10 +331,18 @@ struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } +impl<Highlighter: text::Highlighter> State<Highlighter> { + /// Returns whether the [`TextEditor`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } +} + impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> widget::tree::Tag { @@ -479,7 +513,7 @@ where tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -508,22 +542,22 @@ where Status::Active }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); renderer.fill_editor( &internal.editor, bounds.position() + Vector::new(self.padding.left, self.padding.top), - style.text_color, + defaults.text_color, *viewport, ); @@ -535,27 +569,31 @@ where if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { - let position = position + translation; + let cursor = + Rectangle::new( + position + translation, + Size::new( + 1.0, + self.line_height + .to_absolute(self.text_size.unwrap_or_else( + || renderer.default_size(), + )) + .into(), + ), + ); - if bounds.contains(position) { + if let Some(clipped_cursor) = bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: position.x.floor(), - y: position.y, - width: 1.0, - height: self - .line_height - .to_absolute( - self.text_size.unwrap_or_else( - || renderer.default_size(), - ), - ) - .into(), + x: clipped_cursor.x.floor(), + y: clipped_cursor.y, + width: clipped_cursor.width, + height: clipped_cursor.height, }, ..renderer::Quad::default() }, - appearance.value, + style.value, ); } } @@ -568,7 +606,7 @@ where bounds: range, ..renderer::Quad::default() }, - appearance.selection, + style.selection, ); } } @@ -604,7 +642,7 @@ impl<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Message: 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer, { fn from( @@ -796,7 +834,7 @@ pub enum Status { /// The appearance of a text input. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the text input. pub background: Background, /// The [`Border`] of the text input. @@ -811,32 +849,38 @@ pub struct Appearance { pub selection: Color, } -/// The style of a [`TextEditor`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`TextEditor`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`TextEditor`]. -pub trait DefaultStyle { - /// Returns the default style of a [`TextEditor`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`TextEditor`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`TextEditor`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let active = Appearance { + let active = Style { background: Background::Color(palette.background.base.color), border: Border { radius: 2.0.into(), @@ -851,21 +895,21 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { match status { Status::Active => active, - Status::Hovered => Appearance { + Status::Hovered => Style { border: Border { color: palette.background.base.text, ..active.border }, ..active }, - Status::Focused => Appearance { + Status::Focused => Style { border: Border { color: palette.primary.strong.color, ..active.border }, ..active }, - Status::Disabled => Appearance { + Status::Disabled => Style { background: Background::Color(palette.background.weak.color), value: active.placeholder, ..active diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index b161ec74..e9f07838 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -60,6 +60,7 @@ pub struct TextInput< Theme = crate::Theme, Renderer = crate::Renderer, > where + Theme: Catalog, Renderer: text::Renderer, { id: Option<Id>, @@ -75,7 +76,7 @@ pub struct TextInput< on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, icon: Option<Icon<Renderer::Font>>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } /// The default [`Padding`] of a [`TextInput`]. @@ -84,24 +85,12 @@ pub const DEFAULT_PADDING: Padding = Padding::new(5.0); impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer> where Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { /// Creates a new [`TextInput`] with the given placeholder and /// its current value. - pub fn new(placeholder: &str, value: &str) -> Self - where - Theme: DefaultStyle + 'a, - { - Self::with_style(placeholder, value, Theme::default_style) - } - - /// Creates a new [`TextInput`] with the given placeholder, - /// its current value, and its style. - pub fn with_style( - placeholder: &str, - value: &str, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { + pub fn new(placeholder: &str, value: &str) -> Self { TextInput { id: None, placeholder: String::from(placeholder), @@ -116,7 +105,7 @@ where on_paste: None, on_submit: None, icon: None, - style: Box::new(style), + class: Theme::default(), } } @@ -203,11 +192,19 @@ where } /// Sets the style of the [`TextInput`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`TextInput`]. + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } @@ -235,7 +232,7 @@ where let placeholder_text = Text { font, line_height: self.line_height, - content: &self.placeholder, + content: self.placeholder.as_str(), bounds: Size::new(f32::INFINITY, text_bounds.height), size: text_size, horizontal_alignment: alignment::Horizontal::Left, @@ -254,9 +251,11 @@ where }); if let Some(icon) = &self.icon { + let mut content = [0; 4]; + let icon_text = Text { line_height: self.line_height, - content: &icon.code_point.to_string(), + content: icon.code_point.encode_utf8(&mut content) as &_, font: icon.font, size: icon.size.unwrap_or_else(|| renderer.default_size()), bounds: Size::new(f32::INFINITY, text_bounds.height), @@ -345,15 +344,15 @@ where Status::Active }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); if self.icon.is_some() { @@ -362,14 +361,14 @@ where renderer.fill_paragraph( &state.icon, icon_layout.bounds().center(), - appearance.icon, + style.icon, *viewport, ); } let text = value.to_string(); - let (cursor, offset) = if let Some(focus) = state + let (cursor, offset, is_selecting) = if let Some(focus) = state .is_focused .as_ref() .filter(|focus| focus.is_window_focused) @@ -401,13 +400,13 @@ where }, ..renderer::Quad::default() }, - appearance.value, + style.value, )) } else { None }; - (cursor, offset) + (cursor, offset, false) } cursor::State::Selection { start, end } => { let left = start.min(end); @@ -440,18 +439,19 @@ where }, ..renderer::Quad::default() }, - appearance.selection, + style.selection, )), if end == right { right_offset } else { left_offset }, + true, ) } } } else { - (None, 0.0) + (None, 0.0, false) }; let draw = |renderer: &mut Renderer, viewport| { @@ -475,15 +475,15 @@ where Point::new(text_bounds.x, text_bounds.center_y()) - Vector::new(offset, 0.0), if text.is_empty() { - appearance.placeholder + style.placeholder } else { - appearance.value + style.value }, viewport, ); }; - if cursor.is_some() { + if is_selecting { renderer .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); } else { @@ -496,6 +496,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextInput<'a, Message, Theme, Renderer> where Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -712,7 +713,8 @@ where match key.as_ref() { keyboard::Key::Character("c") - if state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() + && !self.is_secure => { if let Some((start, end)) = state.cursor.selection(&self.value) @@ -726,7 +728,8 @@ where return event::Status::Captured; } keyboard::Key::Character("x") - if state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() + && !self.is_secure => { if let Some((start, end)) = state.cursor.selection(&self.value) @@ -1058,8 +1061,8 @@ where impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where - Message: 'a + Clone, - Theme: 'a, + Message: Clone + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -1400,7 +1403,7 @@ pub enum Status { /// The appearance of a text input. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the text input. pub background: Background, /// The [`Border`] of the text input. @@ -1415,32 +1418,40 @@ pub struct Appearance { pub selection: Color, } -/// The style of a [`TextInput`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`TextInput`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`TextInput`]. -pub trait DefaultStyle { - /// Returns the default style of a [`TextInput`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`TextInput`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`TextInput`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let active = Appearance { + let active = Style { background: Background::Color(palette.background.base.color), border: Border { radius: 2.0.into(), @@ -1455,21 +1466,21 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { match status { Status::Active => active, - Status::Hovered => Appearance { + Status::Hovered => Style { border: Border { color: palette.background.base.text, ..active.border }, ..active }, - Status::Focused => Appearance { + Status::Focused => Style { border: Border { color: palette.primary.strong.color, ..active.border }, ..active }, - Status::Disabled => Appearance { + Status::Disabled => Style { background: Background::Color(palette.background.weak.color), value: active.placeholder, ..active diff --git a/widget/src/themer.rs b/widget/src/themer.rs index a7eabd2c..f4597458 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -155,9 +155,9 @@ where if let Some(background) = self.background { container::draw_background( renderer, - &container::Appearance { + &container::Style { background: Some(background(&theme)), - ..container::Appearance::default() + ..container::Style::default() }, layout.bounds(), ); diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index fc9e06e1..ca6e37b0 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -35,6 +35,7 @@ pub struct Toggler< Theme = crate::Theme, Renderer = crate::Renderer, > where + Theme: Catalog, Renderer: text::Renderer, { is_toggled: bool, @@ -48,11 +49,12 @@ pub struct Toggler< text_shaping: text::Shaping, spacing: f32, font: Option<Renderer::Font>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { /// The default size of a [`Toggler`]. @@ -72,7 +74,6 @@ where f: F, ) -> Self where - Theme: 'a + DefaultStyle, F: 'a + Fn(bool) -> Message, { Toggler { @@ -87,7 +88,7 @@ where text_shaping: text::Shaping::Basic, spacing: Self::DEFAULT_SIZE / 2.0, font: None, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -145,11 +146,20 @@ where } /// Sets the style of the [`Toggler`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Toggler`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -157,6 +167,7 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Toggler<'a, Message, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -284,7 +295,7 @@ where style, label_layout, tree.state.downcast_ref(), - crate::text::Appearance::default(), + crate::text::Style::default(), viewport, ); } @@ -302,7 +313,7 @@ where } }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); let border_radius = bounds.height / BORDER_RADIUS_RATIO; let space = SPACE_RATIO * bounds.height; @@ -319,12 +330,12 @@ where bounds: toggler_background_bounds, border: Border { radius: border_radius.into(), - width: appearance.background_border_width, - color: appearance.background_border_color, + width: style.background_border_width, + color: style.background_border_color, }, ..renderer::Quad::default() }, - appearance.background, + style.background, ); let toggler_foreground_bounds = Rectangle { @@ -344,12 +355,12 @@ where bounds: toggler_foreground_bounds, border: Border { radius: border_radius.into(), - width: appearance.foreground_border_width, - color: appearance.foreground_border_color, + width: style.foreground_border_width, + color: style.foreground_border_color, }, ..renderer::Quad::default() }, - appearance.foreground, + style.foreground, ); } } @@ -358,7 +369,7 @@ impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -385,7 +396,7 @@ pub enum Status { /// The appearance of a toggler. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The background [`Color`] of the toggler. pub background: Color, /// The width of the background border of the toggler. @@ -400,29 +411,37 @@ pub struct Appearance { pub foreground_border_color: Color, } -/// The style of a [`Toggler`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Toggler`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Toggler`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Toggler`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`Toggler`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`Toggler`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let background = match status { @@ -455,7 +474,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { } }; - Appearance { + Style { background, foreground, foreground_border_width: 0.0, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 32c962fc..39f2e07d 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -20,6 +20,7 @@ pub struct Tooltip< Theme = crate::Theme, Renderer = crate::Renderer, > where + Theme: container::Catalog, Renderer: text::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -28,11 +29,12 @@ pub struct Tooltip< gap: f32, padding: f32, snap_within_viewport: bool, - style: container::Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> where + Theme: container::Catalog, Renderer: text::Renderer, { /// The default padding of a [`Tooltip`] drawn by this renderer. @@ -45,10 +47,7 @@ where content: impl Into<Element<'a, Message, Theme, Renderer>>, tooltip: impl Into<Element<'a, Message, Theme, Renderer>>, position: Position, - ) -> Self - where - Theme: container::DefaultStyle + 'a, - { + ) -> Self { Tooltip { content: content.into(), tooltip: tooltip.into(), @@ -56,7 +55,7 @@ where gap: 0.0, padding: Self::DEFAULT_PADDING, snap_within_viewport: true, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -79,11 +78,23 @@ where } /// Sets the style of the [`Tooltip`]. + #[must_use] pub fn style( mut self, - style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + style: impl Fn(&Theme) -> container::Style + 'a, + ) -> Self + where + Theme::Class<'a>: From<container::StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Tooltip`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -91,6 +102,7 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tooltip<'a, Message, Theme, Renderer> where + Theme: container::Catalog, Renderer: text::Renderer, { fn children(&self) -> Vec<widget::Tree> { @@ -239,7 +251,7 @@ where positioning: self.position, gap: self.gap, padding: self.padding, - style: &self.style, + class: &self.class, }))) } else { None @@ -262,7 +274,7 @@ impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, + Theme: container::Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -299,6 +311,7 @@ enum State { struct Overlay<'a, 'b, Message, Theme, Renderer> where + Theme: container::Catalog, Renderer: text::Renderer, { position: Point, @@ -310,14 +323,14 @@ where positioning: Position, gap: f32, padding: f32, - style: - &'b (dyn Fn(&Theme, container::Status) -> container::Appearance + 'a), + class: &'b Theme::Class<'a>, } impl<'a, 'b, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> for Overlay<'a, 'b, Message, Theme, Renderer> where + Theme: container::Catalog, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -426,7 +439,7 @@ where layout: Layout<'_>, cursor_position: mouse::Cursor, ) { - let style = (self.style)(theme, container::Status::Idle); + let style = theme.style(self.class); container::draw_background(renderer, &style, layout.bounds()); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 2aa8f4d1..defb442f 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -2,10 +2,9 @@ use std::ops::RangeInclusive; pub use crate::slider::{ - default, Appearance, DefaultStyle, Handle, HandleShape, Status, Style, + default, Catalog, Handle, HandleShape, Status, Style, StyleFn, }; -use crate::core; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -15,8 +14,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size, - Widget, + self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, + Size, Widget, }; /// An vertical bar and a handle that selects a single value from a range of @@ -41,7 +40,10 @@ use crate::core::{ /// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); /// ``` #[allow(missing_debug_implementations)] -pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> { +pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> +where + Theme: Catalog, +{ range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -51,13 +53,14 @@ pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> { on_release: Option<Message>, width: f32, height: Length, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, + Theme: Catalog, { /// The default width of a [`VerticalSlider`]. pub const DEFAULT_WIDTH: f32 = 16.0; @@ -72,7 +75,6 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where - Theme: DefaultStyle + 'a, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -97,7 +99,7 @@ where on_release: None, width: Self::DEFAULT_WIDTH, height: Length::Fill, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -132,15 +134,6 @@ where self } - /// Sets the style of the [`VerticalSlider`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); - self - } - /// Sets the step size of the [`VerticalSlider`]. pub fn step(mut self, step: T) -> Self { self.step = step; @@ -154,6 +147,24 @@ where self.shift_step = Some(shift_step.into()); self } + + /// Sets the style of the [`VerticalSlider`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`VerticalSlider`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -161,6 +172,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, + Theme: Catalog, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { @@ -354,8 +366,8 @@ where let bounds = layout.bounds(); let is_mouse_over = cursor.is_over(bounds); - let style = (self.style)( - theme, + let style = theme.style( + &self.class, if state.is_dragging { Status::Dragged } else if is_mouse_over { @@ -467,7 +479,7 @@ impl<'a, T, Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, Message: Clone + 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: core::Renderer + 'a, { fn from( |