summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2024-05-23 13:29:45 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2024-05-23 13:29:45 +0200
commitd8ba6b0673a33724a177f3a1ba59705527280142 (patch)
tree89482c8d1e3a03e00b3a8151abbb81e30ae5898c /widget
parent72ed8bcc8def9956e25f3720a3095fc96bb2eef0 (diff)
parent468794d918eb06c1dbebb33c32b10017ad335f05 (diff)
downloadiced-d8ba6b0673a33724a177f3a1ba59705527280142.tar.gz
iced-d8ba6b0673a33724a177f3a1ba59705527280142.tar.bz2
iced-d8ba6b0673a33724a177f3a1ba59705527280142.zip
Merge branch 'master' into feat/text-macro
Diffstat (limited to 'widget')
-rw-r--r--widget/Cargo.toml5
-rw-r--r--widget/src/button.rs143
-rw-r--r--widget/src/canvas.rs37
-rw-r--r--widget/src/canvas/program.rs5
-rw-r--r--widget/src/checkbox.rs101
-rw-r--r--widget/src/column.rs9
-rw-r--r--widget/src/combo_box.rs193
-rw-r--r--widget/src/container.rs233
-rw-r--r--widget/src/helpers.rs517
-rw-r--r--widget/src/image.rs93
-rw-r--r--widget/src/image/viewer.rs23
-rw-r--r--widget/src/keyed/column.rs48
-rw-r--r--widget/src/lazy.rs21
-rw-r--r--widget/src/lazy/component.rs2
-rw-r--r--widget/src/lazy/responsive.rs45
-rw-r--r--widget/src/lib.rs10
-rw-r--r--widget/src/mouse_area.rs2
-rw-r--r--widget/src/overlay/menu.rs179
-rw-r--r--widget/src/pane_grid.rs108
-rw-r--r--widget/src/pane_grid/content.rs59
-rw-r--r--widget/src/pane_grid/state.rs8
-rw-r--r--widget/src/pane_grid/title_bar.rs64
-rw-r--r--widget/src/pick_list.rs110
-rw-r--r--widget/src/progress_bar.rs103
-rw-r--r--widget/src/qr_code.rs101
-rw-r--r--widget/src/radio.rs87
-rw-r--r--widget/src/row.rs9
-rw-r--r--widget/src/rule.rs109
-rw-r--r--widget/src/scrollable.rs511
-rw-r--r--widget/src/shader.rs11
-rw-r--r--widget/src/shader/program.rs4
-rw-r--r--widget/src/slider.rs93
-rw-r--r--widget/src/stack.rs333
-rw-r--r--widget/src/svg.rs175
-rw-r--r--widget/src/text_editor.rs150
-rw-r--r--widget/src/text_input.rs127
-rw-r--r--widget/src/themer.rs4
-rw-r--r--widget/src/toggler.rs85
-rw-r--r--widget/src/tooltip.rs41
-rw-r--r--widget/src/vertical_slider.rs52
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;
///
/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
#[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;
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[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(