diff options
Diffstat (limited to '')
87 files changed, 3221 insertions, 2494 deletions
@@ -18,9 +18,11 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "fira-sans", "auto-detect-theme"] +default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] +# Enable the `tiny-skia` software renderer backend +tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `Image` widget image = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget @@ -50,7 +52,7 @@ highlighter = ["iced_highlighter"] # Enables experimental multi-window support. multi-window = ["iced_winit/multi-window"] # Enables the advanced module -advanced = [] +advanced = ["iced_core/advanced", "iced_widget/advanced"] # Enables embedding Fira Sans as the default font on Wasm builds fira-sans = ["iced_renderer/fira-sans"] # Enables auto-detecting light/dark mode for the built-in theme diff --git a/core/Cargo.toml b/core/Cargo.toml index 29a95ad7..32d233ee 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,6 +12,7 @@ keywords.workspace = true [features] auto-detect-theme = ["dep:dark-light"] +advanced = [] [dependencies] bitflags.workspace = true diff --git a/core/src/element.rs b/core/src/element.rs index 989eaa3b..7d918a2e 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -95,7 +95,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// /// ```no_run /// # mod iced { - /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>; + /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; /// # /// # pub mod widget { /// # pub fn row<'a, Message>(iter: impl IntoIterator<Item = super::Element<'a, Message>>) -> super::Element<'a, Message> { @@ -109,7 +109,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// # pub enum Message {} /// # pub struct Counter; /// # - /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>; + /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; /// # /// # impl Counter { /// # pub fn view(&self) -> Element<Message> { diff --git a/core/src/image.rs b/core/src/image.rs index e5fdcd83..32b95f03 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -186,11 +186,11 @@ pub trait Renderer: crate::Renderer { type Handle: Clone + Hash; /// Returns the dimensions of an image for the given [`Handle`]. - fn dimensions(&self, handle: &Self::Handle) -> Size<u32>; + fn measure_image(&self, handle: &Self::Handle) -> Size<u32>; /// Draws an image with the given [`Handle`] and inside the provided /// `bounds`. - fn draw( + fn draw_image( &mut self, handle: Self::Handle, filter_method: FilterMethod, diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 1139b41c..6712314e 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -2,26 +2,47 @@ #[cfg(debug_assertions)] mod null; -#[cfg(debug_assertions)] -pub use null::Null; - use crate::{ Background, Border, Color, Rectangle, Shadow, Size, Transformation, Vector, }; /// A component that can be used by widgets to draw themselves on a screen. -pub trait Renderer: Sized { +pub trait Renderer { + /// Starts recording a new layer. + fn start_layer(&mut self); + + /// Ends recording a new layer. + /// + /// The new layer will clip its contents to the provided `bounds`. + fn end_layer(&mut self, bounds: Rectangle); + /// Draws the primitives recorded in the given closure in a new layer. /// /// The layer will clip its contents to the provided `bounds`. - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)); + fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { + self.start_layer(); + f(self); + self.end_layer(bounds); + } + + /// Starts recording with a new [`Transformation`]. + fn start_transformation(&mut self); + + /// Ends recording a new layer. + /// + /// The new layer will clip its contents to the provided `bounds`. + fn end_transformation(&mut self, transformation: Transformation); /// Applies a [`Transformation`] to the primitives recorded in the given closure. fn with_transformation( &mut self, transformation: Transformation, f: impl FnOnce(&mut Self), - ); + ) { + self.start_transformation(); + f(self); + self.end_transformation(transformation); + } /// Applies a translation to the primitives recorded in the given closure. fn with_translation( diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 83688ff7..c26ce1a5 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -1,5 +1,7 @@ use crate::alignment; +use crate::image; use crate::renderer::{self, Renderer}; +use crate::svg; use crate::text::{self, Text}; use crate::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, @@ -7,28 +9,14 @@ use crate::{ use std::borrow::Cow; -/// A renderer that does nothing. -/// -/// It can be useful if you are writing tests! -#[derive(Debug, Clone, Copy, Default)] -pub struct Null; +impl Renderer for () { + fn start_layer(&mut self) {} -impl Null { - /// Creates a new [`Null`] renderer. - pub fn new() -> Null { - Null - } -} + fn end_layer(&mut self, _bounds: Rectangle) {} -impl Renderer for Null { - fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} + fn start_transformation(&mut self) {} - fn with_transformation( - &mut self, - _transformation: Transformation, - _f: impl FnOnce(&mut Self), - ) { - } + fn end_transformation(&mut self, _transformation: Transformation) {} fn clear(&mut self) {} @@ -40,7 +28,7 @@ impl Renderer for Null { } } -impl text::Renderer for Null { +impl text::Renderer for () { type Font = Font; type Paragraph = (); type Editor = (); @@ -174,3 +162,33 @@ impl text::Editor for () { ) { } } + +impl image::Renderer for () { + type Handle = (); + + fn measure_image(&self, _handle: &Self::Handle) -> Size<u32> { + Size::default() + } + + fn draw_image( + &mut self, + _handle: Self::Handle, + _filter_method: image::FilterMethod, + _bounds: Rectangle, + ) { + } +} + +impl svg::Renderer for () { + fn measure_svg(&self, _handle: &svg::Handle) -> Size<u32> { + Size::default() + } + + fn draw_svg( + &mut self, + _handle: svg::Handle, + _color: Option<Color>, + _bounds: Rectangle, + ) { + } +} diff --git a/core/src/size.rs b/core/src/size.rs index 267fc90e..55db759d 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,7 +1,7 @@ use crate::Vector; /// An amount of space in 2 dimensions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct Size<T = f32> { /// The width. pub width: T, diff --git a/core/src/svg.rs b/core/src/svg.rs index d63e3c95..ab207cca 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -91,8 +91,13 @@ impl std::fmt::Debug for Data { /// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an SVG for the given [`Handle`]. - fn dimensions(&self, handle: &Handle) -> Size<u32>; + fn measure_svg(&self, handle: &Handle) -> Size<u32>; /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw(&mut self, handle: Handle, color: Option<Color>, bounds: Rectangle); + fn draw_svg( + &mut self, + handle: Handle, + color: Option<Color>, + bounds: Rectangle, + ); } diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 66e2d066..12f6956a 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -18,6 +18,7 @@ pub use text::{LineHeight, Shaping}; #[allow(missing_debug_implementations)] pub struct Text<'a, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { content: Cow<'a, str>, @@ -29,18 +30,16 @@ where vertical_alignment: alignment::Vertical, font: Option<Renderer::Font>, shaping: Shaping, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Theme, Renderer> Text<'a, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { /// Create a new fragment of [`Text`] with the given contents. - pub fn new(content: impl Into<Cow<'a, str>>) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(content: impl Into<Cow<'a, str>>) -> Self { Text { content: content.into(), size: None, @@ -51,7 +50,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::Basic, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -75,25 +74,6 @@ where self } - /// Sets the style of the [`Text`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); - self - } - - /// Sets the [`Color`] of the [`Text`]. - pub fn color(self, color: impl Into<Color>) -> Self { - self.color_maybe(Some(color)) - } - - /// Sets the [`Color`] of the [`Text`], if `Some`. - pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self { - let color = color.map(Into::into); - - self.style = Box::new(move |_theme| Appearance { color }); - self - } - /// Sets the width of the [`Text`] boundaries. pub fn width(mut self, width: impl Into<Length>) -> Self { self.width = width.into(); @@ -129,6 +109,42 @@ where self.shaping = shaping; self } + + /// Sets the style of the [`Text`]. + #[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 [`Color`] of the [`Text`]. + pub fn color(self, color: impl Into<Color>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.color_maybe(Some(color)) + } + + /// Sets the [`Color`] of the [`Text`], if `Some`. + pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + let color = color.map(Into::into); + + self.style(move |_theme| Style { color }) + } + + /// Sets the style class of the [`Text`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } /// The internal state of a [`Text`] widget. @@ -138,6 +154,7 @@ pub struct State<P: Paragraph>(P); impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Text<'a, Theme, Renderer> where + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -182,15 +199,15 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, _cursor_position: mouse::Cursor, viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); - let appearance = (self.style)(theme); + let style = theme.style(&self.class); - draw(renderer, style, layout, state, appearance, viewport); + draw(renderer, defaults, layout, state, style, viewport); } } @@ -250,7 +267,7 @@ pub fn draw<Renderer>( style: &renderer::Style, layout: Layout<'_>, state: &State<Renderer::Paragraph>, - appearance: Appearance, + appearance: Style, viewport: &Rectangle, ) where Renderer: text::Renderer, @@ -281,7 +298,7 @@ pub fn draw<Renderer>( impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -293,7 +310,7 @@ where impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer> where - Theme: DefaultStyle + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer, { fn from(content: &'a str) -> Self { @@ -304,7 +321,7 @@ where impl<'a, Message, Theme, Renderer> From<&'a str> for Element<'a, Message, Theme, Renderer> where - Theme: DefaultStyle + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from(content: &'a str) -> Self { @@ -314,30 +331,38 @@ where /// The appearance of some text. #[derive(Debug, Clone, Copy, Default)] -pub struct Appearance { +pub struct Style { /// The [`Color`] of the text. /// /// The default, `None`, means using the inherited color. pub color: Option<Color>, } -/// The style of some [`Text`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`Text`]. +pub trait Catalog: Sized { + /// The item class of this [`Catalog`]. + type Class<'a>; -/// The default style of some [`Text`]. -pub trait DefaultStyle { - /// Returns the default style of some [`Text`]. - fn default_style(&self) -> Appearance; + /// The default class produced by this [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, item: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - Appearance::default() +/// A styling function for a [`Text`]. +/// +/// 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(|_theme| Style::default()) } -} -impl DefaultStyle for Color { - fn default_style(&self) -> Appearance { - Appearance { color: Some(*self) } + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index cf70bd40..289c919b 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -143,23 +143,18 @@ mod bezier { bounds: Rectangle, cursor: mouse::Cursor, ) -> Vec<Geometry> { - let content = self.state.cache.draw( - renderer, - bounds.size(), - |frame: &mut Frame| { + let content = + self.state.cache.draw(renderer, bounds.size(), |frame| { Curve::draw_all(self.curves, frame); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), Stroke::default().with_width(2.0), ); - }, - ); + }); if let Some(pending) = state { - let pending_curve = pending.draw(renderer, bounds, cursor); - - vec![content, pending_curve] + vec![content, pending.draw(renderer, bounds, cursor)] } else { vec![content] } diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 43ba3187..b2c71b3f 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -73,10 +73,7 @@ mod numeric_input { impl<Message, Theme> Component<Message, Theme> for NumericInput<Message> where - Theme: text::DefaultStyle - + button::DefaultStyle - + text_input::DefaultStyle - + 'static, + Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, { type State = (); type Event = Event; @@ -151,10 +148,7 @@ mod numeric_input { impl<'a, Message, Theme> From<NumericInput<Message>> for Element<'a, Message, Theme> where - Theme: text::DefaultStyle - + button::DefaultStyle - + text_input::DefaultStyle - + 'static, + Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, Message: 'a, { fn from(numeric_input: NumericInput<Message>) -> Self { diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 2b0fae0b..0716b2a4 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -602,9 +602,7 @@ mod grid { frame.into_geometry() }; - if self.scaling < 0.2 || !self.show_lines { - vec![life, overlay] - } else { + if self.scaling >= 0.2 && self.show_lines { let grid = self.grid_cache.draw(renderer, bounds.size(), |frame| { frame.translate(center); @@ -641,6 +639,8 @@ mod grid { }); vec![life, grid, overlay] + } else { + vec![life, overlay] } } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 63efcbdd..16cdb86f 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -44,7 +44,9 @@ mod rainbow { cursor: mouse::Cursor, _viewport: &Rectangle, ) { - use iced::advanced::graphics::mesh::{self, Mesh, SolidVertex2D}; + use iced::advanced::graphics::mesh::{ + self, Mesh, Renderer as _, SolidVertex2D, + }; use iced::advanced::Renderer as _; let bounds = layout.bounds(); diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 22c21cdd..2b906c32 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -60,7 +60,7 @@ impl Gradient { } = *self; let gradient_box = container(horizontal_space()) - .style(move |_theme, _status| { + .style(move |_theme| { let gradient = gradient::Linear::new(angle) .add_stop(0.0, start) .add_stop(1.0, end); diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 713e2b70..66d79091 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -81,10 +81,10 @@ impl Layout { } else { self.example.view() }) - .style(|theme, _status| { + .style(|theme| { let palette = theme.extended_palette(); - container::Appearance::default() + container::Style::default() .with_border(palette.background.strong.color, 4.0) }) .padding(4) @@ -245,10 +245,10 @@ fn application<'a>() -> Element<'a, Message> { .padding(10) .align_items(Alignment::Center), ) - .style(|theme, _status| { + .style(|theme| { let palette = theme.extended_palette(); - container::Appearance::default() + container::Style::default() .with_border(palette.background.strong.color, 1) }); diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 12670ed1..de728af2 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -358,7 +358,7 @@ where |renderer| { use iced::advanced::graphics::geometry::Renderer as _; - renderer.draw(vec![geometry]); + renderer.draw_geometry(geometry); }, ); } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 829996d8..17112785 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -338,39 +338,30 @@ mod style { use iced::widget::container; use iced::{Border, Theme}; - pub fn title_bar_active( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + pub fn title_bar_active(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { text_color: Some(palette.background.strong.text), background: Some(palette.background.strong.color.into()), ..Default::default() } } - pub fn title_bar_focused( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + pub fn title_bar_focused(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { text_color: Some(palette.primary.strong.text), background: Some(palette.primary.strong.color.into()), ..Default::default() } } - pub fn pane_active( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + pub fn pane_active(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { background: Some(palette.background.weak.color.into()), border: Border { width: 2.0, @@ -381,13 +372,10 @@ mod style { } } - pub fn pane_focused( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + pub fn pane_focused(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { background: Some(palette.background.weak.color.into()), border: Border { width: 2.0, diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 240ae908..c02e754b 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -341,8 +341,8 @@ impl Default for ScrollableDemo { } } -fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance { - progress_bar::Appearance { +fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Style { + progress_bar::Style { background: theme.extended_palette().background.strong.color.into(), bar: Color::from_rgb8(250, 85, 134).into(), border: Border::default(), diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 07ae05d6..9cd6237f 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -1,6 +1,6 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{self, Canvas}; +use iced::widget::canvas::{self, Canvas, Geometry}; use iced::widget::{column, row, slider, text}; use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme}; @@ -111,7 +111,7 @@ impl canvas::Program<Message> for SierpinskiGraph { _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec<canvas::Geometry> { + ) -> Vec<Geometry> { let geom = self.cache.draw(renderer, bounds.size(), |frame| { frame.stroke( &canvas::Path::rectangle(Point::ORIGIN, frame.size()), diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index b5228f09..deb211d8 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -10,7 +10,7 @@ use iced::mouse; use iced::widget::canvas; use iced::widget::canvas::gradient; use iced::widget::canvas::stroke::{self, Stroke}; -use iced::widget::canvas::Path; +use iced::widget::canvas::{Geometry, Path}; use iced::window; use iced::{ Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription, @@ -130,7 +130,7 @@ impl<Message> canvas::Program<Message> for State { _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec<canvas::Geometry> { + ) -> Vec<Geometry> { use std::f32::consts::PI; let background = diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index cc686dca..0dcf9bc1 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -31,7 +31,7 @@ impl Tiger { )); let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( - |_theme, _status| svg::Appearance { + |_theme, _status| svg::Style { color: if self.apply_color_filter { Some(color!(0x0000ff)) } else { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index fdae1dc1..9968962c 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -651,45 +651,33 @@ mod toast { } } - fn styled(pair: theme::palette::Pair) -> container::Appearance { - container::Appearance { + fn styled(pair: theme::palette::Pair) -> container::Style { + container::Style { background: Some(pair.color.into()), text_color: pair.text.into(), ..Default::default() } } - fn primary( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + fn primary(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); styled(palette.primary.weak) } - fn secondary( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + fn secondary(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); styled(palette.secondary.weak) } - fn success( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + fn success(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); styled(palette.success.weak) } - fn danger( - theme: &Theme, - _status: container::Status, - ) -> container::Appearance { + fn danger(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); styled(palette.danger.weak) diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 10eb337f..7abc42c5 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -2,15 +2,19 @@ use crate::core::image; use crate::core::svg; use crate::core::Size; +use crate::{Compositor, Mesh, Renderer}; use std::borrow::Cow; /// The graphics backend of a [`Renderer`]. /// /// [`Renderer`]: crate::Renderer -pub trait Backend { +pub trait Backend: Sized { /// The custom kind of primitives this [`Backend`] supports. - type Primitive; + type Primitive: TryFrom<Mesh, Error = &'static str>; + + /// The default compositor of this [`Backend`]. + type Compositor: Compositor<Renderer = Renderer<Self>>; } /// A graphics backend that supports text rendering. diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs new file mode 100644 index 00000000..b52f9d9d --- /dev/null +++ b/graphics/src/cached.rs @@ -0,0 +1,42 @@ +use crate::Primitive; + +use std::sync::Arc; + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self) -> Self::Cache; +} + +impl<T> Cached for Primitive<T> { + type Cache = Arc<Self>; + + fn load(cache: &Arc<Self>) -> Self { + Self::Cache { + content: cache.clone(), + } + } + + fn cache(self) -> Arc<Self> { + Arc::new(self) + } +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache(self) -> Self::Cache {} +} diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 91951a8e..86472a58 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -1,9 +1,8 @@ //! A compositor is responsible for initializing a renderer and managing window //! surfaces. -use crate::{Error, Viewport}; - use crate::core::Color; use crate::futures::{MaybeSend, MaybeSync}; +use crate::{Error, Settings, Viewport}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::future::Future; @@ -11,19 +10,28 @@ use thiserror::Error; /// A graphics compositor that can draw to windows. pub trait Compositor: Sized { - /// The settings of the backend. - type Settings: Default; - /// The iced renderer of the backend. - type Renderer: iced_core::Renderer; + type Renderer; /// The surface of the backend. type Surface; /// Creates a new [`Compositor`]. fn new<W: Window + Clone>( - settings: Self::Settings, + settings: Settings, compatible_window: W, + ) -> impl Future<Output = Result<Self, Error>> { + Self::with_backend(settings, compatible_window, None) + } + + /// Creates a new [`Compositor`] with a backend preference. + /// + /// If the backend does not match the preference, it will return + /// [`Error::GraphicsAdapterNotFound`]. + fn with_backend<W: Window + Clone>( + _settings: Settings, + _compatible_window: W, + _backend: Option<&str>, ) -> impl Future<Output = Result<Self, Error>>; /// Creates a [`Self::Renderer`] for the [`Compositor`]. @@ -93,6 +101,12 @@ impl<T> Window for T where { } +/// Defines the default compositor of a renderer. +pub trait Default { + /// The compositor of the renderer. + type Compositor: Compositor<Renderer = Self>; +} + /// Result of an unsuccessful call to [`Compositor::present`]. #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum SurfaceError { @@ -122,3 +136,69 @@ pub struct Information { /// Contains the graphics backend. pub backend: String, } + +#[cfg(debug_assertions)] +impl Compositor for () { + type Renderer = (); + type Surface = (); + + async fn with_backend<W: Window + Clone>( + _settings: Settings, + _compatible_window: W, + _preffered_backend: Option<&str>, + ) -> Result<Self, Error> { + Ok(()) + } + + fn create_renderer(&self) -> Self::Renderer {} + + fn create_surface<W: Window + Clone>( + &mut self, + _window: W, + _width: u32, + _height: u32, + ) -> Self::Surface { + } + + fn configure_surface( + &mut self, + _surface: &mut Self::Surface, + _width: u32, + _height: u32, + ) { + } + + fn fetch_information(&self) -> Information { + Information { + adapter: String::from("Null Renderer"), + backend: String::from("Null"), + } + } + + fn present<T: AsRef<str>>( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + _overlay: &[T], + ) -> Result<(), SurfaceError> { + Ok(()) + } + + fn screenshot<T: AsRef<str>>( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + _overlay: &[T], + ) -> Vec<u8> { + vec![] + } +} + +#[cfg(debug_assertions)] +impl Default for () { + type Compositor = (); +} diff --git a/graphics/src/error.rs b/graphics/src/error.rs index c6ea98a3..6ea1d3a4 100644 --- a/graphics/src/error.rs +++ b/graphics/src/error.rs @@ -1,5 +1,7 @@ +//! See what can go wrong when creating graphical backends. + /// An error that occurred while creating an application's graphical context. -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum Error { /// The requested backend version is not supported. #[error("the requested backend version is not supported")] @@ -11,9 +13,30 @@ pub enum Error { /// A suitable graphics adapter or device could not be found. #[error("a suitable graphics adapter or device could not be found")] - GraphicsAdapterNotFound, + GraphicsAdapterNotFound { + /// The name of the backend where the error happened + backend: &'static str, + /// The reason why this backend could not be used + reason: Reason, + }, /// An error occurred in the context's internal backend #[error("an error occurred in the context's internal backend")] BackendError(String), + + /// Multiple errors occurred + #[error("multiple errors occurred: {0:?}")] + List(Vec<Self>), +} + +/// The reason why a graphics adapter could not be found +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Reason { + /// The backend did not match the preference + DidNotMatch { + /// The preferred backend + preferred_backend: String, + }, + /// The request to create the backend failed + RequestFailed(String), } diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index d7d6a0aa..d251efb8 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -1,12 +1,16 @@ //! Build and draw geometry. pub mod fill; +pub mod frame; pub mod path; pub mod stroke; +mod cache; mod style; mod text; +pub use cache::Cache; pub use fill::Fill; +pub use frame::Frame; pub use path::Path; pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; @@ -14,11 +18,39 @@ pub use text::Text; pub use crate::gradient::{self, Gradient}; +use crate::core::{self, Size}; +use crate::Cached; + /// A renderer capable of drawing some [`Self::Geometry`]. -pub trait Renderer: crate::core::Renderer { +pub trait Renderer: core::Renderer { /// The kind of geometry this renderer can draw. - type Geometry; + type Geometry: Cached; + + /// The kind of [`Frame`] this renderer supports. + type Frame: frame::Backend<Geometry = Self::Geometry>; + + /// Creates a new [`Self::Frame`]. + fn new_frame(&self, size: Size) -> Self::Frame; + + /// Draws the given [`Self::Geometry`]. + fn draw_geometry(&mut self, geometry: Self::Geometry); +} + +/// The graphics backend of a geometry renderer. +pub trait Backend { + /// The kind of [`Frame`] this backend supports. + type Frame: frame::Backend; + + /// Creates a new [`Self::Frame`]. + fn new_frame(&self, size: Size) -> Self::Frame; +} + +#[cfg(debug_assertions)] +impl Renderer for () { + type Geometry = (); + type Frame = (); + + fn new_frame(&self, _size: Size) -> Self::Frame {} - /// Draws the given layers of [`Self::Geometry`]. - fn draw(&mut self, layers: Vec<Self::Geometry>); + fn draw_geometry(&mut self, _geometry: Self::Geometry) {} } diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs new file mode 100644 index 00000000..37d433c2 --- /dev/null +++ b/graphics/src/geometry/cache.rs @@ -0,0 +1,108 @@ +use crate::core::Size; +use crate::geometry::{self, Frame}; +use crate::Cached; + +use std::cell::RefCell; + +/// 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 struct Cache<Renderer> +where + Renderer: geometry::Renderer, +{ + state: RefCell<State<Renderer::Geometry>>, +} + +impl<Renderer> Cache<Renderer> +where + Renderer: geometry::Renderer, +{ + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: RefCell::new(State::Empty), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws geometry using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored geometry will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored geometry if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame<Renderer>), + ) -> Renderer::Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + geometry, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Cached::load(geometry); + } + } + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let geometry = frame.into_geometry().cache(); + let result = Cached::load(&geometry); + + *self.state.borrow_mut() = State::Filled { bounds, geometry }; + + result + } +} + +impl<Renderer> std::fmt::Debug for Cache<Renderer> +where + Renderer: geometry::Renderer, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let state = self.state.borrow(); + + match *state { + State::Empty => write!(f, "Cache::Empty"), + State::Filled { bounds, .. } => { + write!(f, "Cache::Filled {{ bounds: {bounds:?} }}") + } + } + } +} + +impl<Renderer> Default for Cache<Renderer> +where + Renderer: geometry::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +enum State<Geometry> +where + Geometry: Cached, +{ + Empty, + Filled { + bounds: Size, + geometry: Geometry::Cache, + }, +} diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs new file mode 100644 index 00000000..ad35e8ec --- /dev/null +++ b/graphics/src/geometry/frame.rs @@ -0,0 +1,251 @@ +//! Draw and generate geometry. +use crate::core::{Point, Radians, Rectangle, Size, Vector}; +use crate::geometry::{self, Fill, Path, Stroke, Text}; + +/// The region of a surface that can be used to draw geometry. +#[allow(missing_debug_implementations)] +pub struct Frame<Renderer> +where + Renderer: geometry::Renderer, +{ + raw: Renderer::Frame, +} + +impl<Renderer> Frame<Renderer> +where + Renderer: geometry::Renderer, +{ + /// Creates a new [`Frame`] with the given dimensions. + pub fn new(renderer: &Renderer, size: Size) -> Self { + Self { + raw: renderer.new_frame(size), + } + } + + /// Returns the width of the [`Frame`]. + pub fn width(&self) -> f32 { + self.raw.width() + } + + /// Returns the height of the [`Frame`]. + pub fn height(&self) -> f32 { + self.raw.height() + } + + /// Returns the dimensions of the [`Frame`]. + pub fn size(&self) -> Size { + self.raw.size() + } + + /// Returns the coordinate of the center of the [`Frame`]. + pub fn center(&self) -> Point { + self.raw.center() + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + self.raw.fill(path, fill); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into<Fill>, + ) { + self.raw.fill_rectangle(top_left, size, fill); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + self.raw.stroke(path, stroke); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ All text will be rendered on top of all the layers of + /// a `Canvas`. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + pub fn fill_text(&mut self, text: impl Into<Text>) { + self.raw.fill_text(text); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.push_transform(); + + let result = f(self); + + self.pop_transform(); + + result + } + + /// Pushes the current transform in the transform stack. + pub fn push_transform(&mut self) { + self.raw.push_transform(); + } + + /// Pops a transform from the transform stack and sets it as the current transform. + pub fn pop_transform(&mut self) { + self.raw.pop_transform(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip<R>( + &mut self, + region: Rectangle, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + let mut frame = self.draft(region.size()); + + let result = f(&mut frame); + + let origin = Point::new(region.x, region.y); + + self.paste(frame, origin); + + result + } + + /// Creates a new [`Frame`] with the given [`Size`]. + /// + /// Draw its contents back to this [`Frame`] with [`paste`]. + /// + /// [`paste`]: Self::paste + pub fn draft(&mut self, size: Size) -> Self { + Self { + raw: self.raw.draft(size), + } + } + + /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. + pub fn paste(&mut self, frame: Self, at: Point) { + self.raw.paste(frame.raw, at); + } + + /// Applies a translation to the current transform of the [`Frame`]. + pub fn translate(&mut self, translation: Vector) { + self.raw.translate(translation); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + pub fn rotate(&mut self, angle: impl Into<Radians>) { + self.raw.rotate(angle); + } + + /// Applies a uniform scaling to the current transform of the [`Frame`]. + pub fn scale(&mut self, scale: impl Into<f32>) { + self.raw.scale(scale); + } + + /// Applies a non-uniform scaling to the current transform of the [`Frame`]. + pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + self.raw.scale_nonuniform(scale); + } + + /// Turns the [`Frame`] into its underlying geometry. + pub fn into_geometry(self) -> Renderer::Geometry { + self.raw.into_geometry() + } +} + +/// The internal implementation of a [`Frame`]. +/// +/// Analogous to [`Frame`]. See [`Frame`] for the documentation +/// of each method. +#[allow(missing_docs)] +pub trait Backend: Sized { + type Geometry; + + fn width(&self) -> f32; + fn height(&self) -> f32; + fn size(&self) -> Size; + fn center(&self) -> Point; + + fn push_transform(&mut self); + fn pop_transform(&mut self); + + fn translate(&mut self, translation: Vector); + fn rotate(&mut self, angle: impl Into<Radians>); + fn scale(&mut self, scale: impl Into<f32>); + fn scale_nonuniform(&mut self, scale: impl Into<Vector>); + + fn draft(&mut self, size: Size) -> Self; + fn paste(&mut self, frame: Self, at: Point); + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>); + + fn fill(&mut self, path: &Path, fill: impl Into<Fill>); + fn fill_text(&mut self, text: impl Into<Text>); + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into<Fill>, + ); + + fn into_geometry(self) -> Self::Geometry; +} + +#[cfg(debug_assertions)] +impl Backend for () { + type Geometry = (); + + fn width(&self) -> f32 { + 0.0 + } + + fn height(&self) -> f32 { + 0.0 + } + + fn size(&self) -> Size { + Size::ZERO + } + + fn center(&self) -> Point { + Point::ORIGIN + } + + fn push_transform(&mut self) {} + fn pop_transform(&mut self) {} + + fn translate(&mut self, _translation: Vector) {} + fn rotate(&mut self, _angle: impl Into<Radians>) {} + fn scale(&mut self, _scale: impl Into<f32>) {} + fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {} + + fn draft(&mut self, _size: Size) -> Self {} + fn paste(&mut self, _frame: Self, _at: Point) {} + + fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {} + + fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {} + fn fill_text(&mut self, _text: impl Into<Text>) {} + fn fill_rectangle( + &mut self, + _top_left: Point, + _size: Size, + _fill: impl Into<Fill>, + ) { + } + + fn into_geometry(self) -> Self::Geometry {} +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index aa9d00e8..d7f2f439 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -17,14 +17,16 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; -mod error; +mod cached; mod primitive; +mod settings; mod viewport; pub mod backend; pub mod color; pub mod compositor; pub mod damage; +pub mod error; pub mod gradient; pub mod mesh; pub mod renderer; @@ -38,6 +40,7 @@ pub mod image; pub use antialiasing::Antialiasing; pub use backend::Backend; +pub use cached::Cached; pub use compositor::Compositor; pub use damage::Damage; pub use error::Error; @@ -45,6 +48,7 @@ pub use gradient::Gradient; pub use mesh::Mesh; pub use primitive::Primitive; pub use renderer::Renderer; +pub use settings::Settings; pub use viewport::Viewport; pub use iced_core as core; diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs index 041986cf..20692b07 100644 --- a/graphics/src/mesh.rs +++ b/graphics/src/mesh.rs @@ -74,3 +74,9 @@ pub struct GradientVertex2D { /// The packed vertex data of the gradient. pub gradient: gradient::Packed, } + +/// A renderer capable of drawing a [`Mesh`]. +pub trait Renderer { + /// Draws the given [`Mesh`]. + fn draw_mesh(&mut self, mesh: Mesh); +} diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 143f348b..f517ff3e 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,5 +1,6 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; +use crate::compositor; use crate::core; use crate::core::image; use crate::core::renderer; @@ -8,8 +9,9 @@ use crate::core::text::Text; use crate::core::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; +use crate::mesh; use crate::text; -use crate::Primitive; +use crate::{Mesh, Primitive}; use std::borrow::Cow; @@ -20,6 +22,7 @@ pub struct Renderer<B: Backend> { default_font: Font, default_text_size: Pixels, primitives: Vec<Primitive<B::Primitive>>, + stack: Vec<Vec<Primitive<B::Primitive>>>, } impl<B: Backend> Renderer<B> { @@ -34,6 +37,7 @@ impl<B: Backend> Renderer<B> { default_font, default_text_size, primitives: Vec::new(), + stack: Vec::new(), } } @@ -55,61 +59,35 @@ impl<B: Backend> Renderer<B> { ) -> O { f(&mut self.backend, &self.primitives) } +} - /// Starts recording a new layer. - pub fn start_layer(&mut self) -> Vec<Primitive<B::Primitive>> { - std::mem::take(&mut self.primitives) +impl<B: Backend> iced_core::Renderer for Renderer<B> { + fn start_layer(&mut self) { + self.stack.push(std::mem::take(&mut self.primitives)); } - /// Ends the recording of a layer. - pub fn end_layer( - &mut self, - primitives: Vec<Primitive<B::Primitive>>, - bounds: Rectangle, - ) { - let layer = std::mem::replace(&mut self.primitives, primitives); + fn end_layer(&mut self, bounds: Rectangle) { + let layer = std::mem::replace( + &mut self.primitives, + self.stack.pop().expect("a layer should be recording"), + ); self.primitives.push(Primitive::group(layer).clip(bounds)); } - /// Starts recording a translation. - pub fn start_transformation(&mut self) -> Vec<Primitive<B::Primitive>> { - std::mem::take(&mut self.primitives) + fn start_transformation(&mut self) { + self.stack.push(std::mem::take(&mut self.primitives)); } - /// Ends the recording of a translation. - pub fn end_transformation( - &mut self, - primitives: Vec<Primitive<B::Primitive>>, - transformation: Transformation, - ) { - let layer = std::mem::replace(&mut self.primitives, primitives); + fn end_transformation(&mut self, transformation: Transformation) { + let layer = std::mem::replace( + &mut self.primitives, + self.stack.pop().expect("a layer should be recording"), + ); self.primitives .push(Primitive::group(layer).transform(transformation)); } -} - -impl<B: Backend> iced_core::Renderer for Renderer<B> { - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - let current = self.start_layer(); - - f(self); - - self.end_layer(current, bounds); - } - - fn with_transformation( - &mut self, - transformation: Transformation, - f: impl FnOnce(&mut Self), - ) { - let current = self.start_transformation(); - - f(self); - - self.end_transformation(current, transformation); - } fn fill_quad( &mut self, @@ -211,11 +189,11 @@ where { type Handle = image::Handle; - fn dimensions(&self, handle: &image::Handle) -> Size<u32> { + fn measure_image(&self, handle: &image::Handle) -> Size<u32> { self.backend().dimensions(handle) } - fn draw( + fn draw_image( &mut self, handle: image::Handle, filter_method: image::FilterMethod, @@ -233,11 +211,11 @@ impl<B> svg::Renderer for Renderer<B> where B: Backend + backend::Svg, { - fn dimensions(&self, handle: &svg::Handle) -> Size<u32> { + fn measure_svg(&self, handle: &svg::Handle) -> Size<u32> { self.backend().viewport_dimensions(handle) } - fn draw( + fn draw_svg( &mut self, handle: svg::Handle, color: Option<Color>, @@ -250,3 +228,42 @@ where }); } } + +impl<B: Backend> mesh::Renderer for Renderer<B> { + fn draw_mesh(&mut self, mesh: Mesh) { + match B::Primitive::try_from(mesh) { + Ok(primitive) => { + self.draw_primitive(Primitive::Custom(primitive)); + } + Err(error) => { + log::warn!("mesh primitive could not be drawn: {error:?}"); + } + } + } +} + +#[cfg(feature = "geometry")] +impl<B> crate::geometry::Renderer for Renderer<B> +where + B: Backend + crate::geometry::Backend, + B::Frame: + crate::geometry::frame::Backend<Geometry = Primitive<B::Primitive>>, +{ + type Frame = B::Frame; + type Geometry = Primitive<B::Primitive>; + + fn new_frame(&self, size: Size) -> Self::Frame { + self.backend.new_frame(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + self.draw_primitive(geometry); + } +} + +impl<B> compositor::Default for Renderer<B> +where + B: Backend, +{ + type Compositor = B::Compositor; +} diff --git a/renderer/src/settings.rs b/graphics/src/settings.rs index 432eb8a0..68673536 100644 --- a/renderer/src/settings.rs +++ b/graphics/src/settings.rs @@ -1,5 +1,5 @@ use crate::core::{Font, Pixels}; -use crate::graphics::Antialiasing; +use crate::Antialiasing; /// The settings of a Backend. #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 5cce2427..39c19fa3 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -12,9 +12,10 @@ keywords.workspace = true [features] wgpu = ["iced_wgpu"] -image = ["iced_tiny_skia/image", "iced_wgpu?/image"] -svg = ["iced_tiny_skia/svg", "iced_wgpu?/svg"] -geometry = ["iced_graphics/geometry", "iced_tiny_skia/geometry", "iced_wgpu?/geometry"] +tiny-skia = ["iced_tiny_skia"] +image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] +svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"] +geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"] tracing = ["iced_wgpu?/tracing"] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] @@ -22,7 +23,9 @@ fira-sans = ["iced_graphics/fira-sans"] [dependencies] iced_graphics.workspace = true + iced_tiny_skia.workspace = true +iced_tiny_skia.optional = true iced_wgpu.workspace = true iced_wgpu.optional = true diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index c23a814c..8b137891 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -1,270 +1 @@ -use crate::core::Color; -use crate::graphics::compositor::{Information, SurfaceError, Window}; -use crate::graphics::{Error, Viewport}; -use crate::{Renderer, Settings}; -use std::env; -use std::future::Future; - -pub enum Compositor { - TinySkia(iced_tiny_skia::window::Compositor), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Compositor), -} - -pub enum Surface { - TinySkia(iced_tiny_skia::window::Surface), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Surface<'static>), -} - -impl crate::graphics::Compositor for Compositor { - type Settings = Settings; - type Renderer = Renderer; - type Surface = Surface; - - fn new<W: Window + Clone>( - settings: Self::Settings, - compatible_window: W, - ) -> impl Future<Output = Result<Self, Error>> { - let candidates = - Candidate::list_from_env().unwrap_or(Candidate::default_list()); - - async move { - let mut error = Error::GraphicsAdapterNotFound; - - for candidate in candidates { - match candidate.build(settings, compatible_window.clone()).await - { - Ok(compositor) => return Ok(compositor), - Err(new_error) => { - error = new_error; - } - } - } - - Err(error) - } - } - - fn create_renderer(&self) -> Self::Renderer { - match self { - Compositor::TinySkia(compositor) => { - Renderer::TinySkia(compositor.create_renderer()) - } - #[cfg(feature = "wgpu")] - Compositor::Wgpu(compositor) => { - Renderer::Wgpu(compositor.create_renderer()) - } - } - } - - fn create_surface<W: Window + Clone>( - &mut self, - window: W, - width: u32, - height: u32, - ) -> Surface { - match self { - Self::TinySkia(compositor) => Surface::TinySkia( - compositor.create_surface(window, width, height), - ), - #[cfg(feature = "wgpu")] - Self::Wgpu(compositor) => { - Surface::Wgpu(compositor.create_surface(window, width, height)) - } - } - } - - fn configure_surface( - &mut self, - surface: &mut Surface, - width: u32, - height: u32, - ) { - match (self, surface) { - (Self::TinySkia(compositor), Surface::TinySkia(surface)) => { - compositor.configure_surface(surface, width, height); - } - #[cfg(feature = "wgpu")] - (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { - compositor.configure_surface(surface, width, height); - } - #[allow(unreachable_patterns)] - _ => panic!( - "The provided surface is not compatible with the compositor." - ), - } - } - - fn fetch_information(&self) -> Information { - match self { - Self::TinySkia(compositor) => compositor.fetch_information(), - #[cfg(feature = "wgpu")] - Self::Wgpu(compositor) => compositor.fetch_information(), - } - } - - fn present<T: AsRef<str>>( - &mut self, - renderer: &mut Self::Renderer, - surface: &mut Self::Surface, - viewport: &Viewport, - background_color: Color, - overlay: &[T], - ) -> Result<(), SurfaceError> { - match (self, renderer, surface) { - ( - Self::TinySkia(_compositor), - crate::Renderer::TinySkia(renderer), - Surface::TinySkia(surface), - ) => renderer.with_primitives(|backend, primitives| { - iced_tiny_skia::window::compositor::present( - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) - }), - #[cfg(feature = "wgpu")] - ( - Self::Wgpu(compositor), - crate::Renderer::Wgpu(renderer), - Surface::Wgpu(surface), - ) => renderer.with_primitives(|backend, primitives| { - iced_wgpu::window::compositor::present( - compositor, - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) - }), - #[allow(unreachable_patterns)] - _ => panic!( - "The provided renderer or surface are not compatible \ - with the compositor." - ), - } - } - - fn screenshot<T: AsRef<str>>( - &mut self, - renderer: &mut Self::Renderer, - surface: &mut Self::Surface, - viewport: &Viewport, - background_color: Color, - overlay: &[T], - ) -> Vec<u8> { - match (self, renderer, surface) { - ( - Self::TinySkia(_compositor), - Renderer::TinySkia(renderer), - Surface::TinySkia(surface), - ) => renderer.with_primitives(|backend, primitives| { - iced_tiny_skia::window::compositor::screenshot( - surface, - backend, - primitives, - viewport, - background_color, - overlay, - ) - }), - #[cfg(feature = "wgpu")] - ( - Self::Wgpu(compositor), - Renderer::Wgpu(renderer), - Surface::Wgpu(_), - ) => renderer.with_primitives(|backend, primitives| { - iced_wgpu::window::compositor::screenshot( - compositor, - backend, - primitives, - viewport, - background_color, - overlay, - ) - }), - #[allow(unreachable_patterns)] - _ => panic!( - "The provided renderer or backend are not compatible \ - with the compositor." - ), - } - } -} - -enum Candidate { - Wgpu, - TinySkia, -} - -impl Candidate { - fn default_list() -> Vec<Self> { - vec![ - #[cfg(feature = "wgpu")] - Self::Wgpu, - Self::TinySkia, - ] - } - - fn list_from_env() -> Option<Vec<Self>> { - let backends = env::var("ICED_BACKEND").ok()?; - - Some( - backends - .split(',') - .map(str::trim) - .map(|backend| match backend { - "wgpu" => Self::Wgpu, - "tiny-skia" => Self::TinySkia, - _ => panic!("unknown backend value: \"{backend}\""), - }) - .collect(), - ) - } - - async fn build<W: Window>( - self, - settings: Settings, - _compatible_window: W, - ) -> Result<Compositor, Error> { - match self { - Self::TinySkia => { - let compositor = iced_tiny_skia::window::compositor::new( - iced_tiny_skia::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - }, - _compatible_window, - ); - - Ok(Compositor::TinySkia(compositor)) - } - #[cfg(feature = "wgpu")] - Self::Wgpu => { - let compositor = iced_wgpu::window::compositor::new( - iced_wgpu::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - ..iced_wgpu::Settings::from_env() - }, - _compatible_window, - ) - .await?; - - Ok(Compositor::Wgpu(compositor)) - } - #[cfg(not(feature = "wgpu"))] - Self::Wgpu => { - panic!("`wgpu` feature was not enabled in `iced_renderer`") - } - } - } -} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs new file mode 100644 index 00000000..ef9cc9a9 --- /dev/null +++ b/renderer/src/fallback.rs @@ -0,0 +1,570 @@ +use crate::core::image; +use crate::core::renderer; +use crate::core::svg; +use crate::core::{ + self, Background, Color, Point, Rectangle, Size, Transformation, +}; +use crate::graphics; +use crate::graphics::compositor; +use crate::graphics::mesh; + +pub enum Renderer<L, R> { + Left(L), + Right(R), +} + +macro_rules! delegate { + ($renderer:expr, $name:ident, $body:expr) => { + match $renderer { + Self::Left($name) => $body, + Self::Right($name) => $body, + } + }; +} + +impl<L, R> core::Renderer for Renderer<L, R> +where + L: core::Renderer, + R: core::Renderer, +{ + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into<Background>, + ) { + delegate!(self, renderer, renderer.fill_quad(quad, background.into())); + } + + fn clear(&mut self) { + delegate!(self, renderer, renderer.clear()); + } + + fn start_layer(&mut self) { + delegate!(self, renderer, renderer.start_layer()); + } + + fn end_layer(&mut self, bounds: Rectangle) { + delegate!(self, renderer, renderer.end_layer(bounds)); + } + + fn start_transformation(&mut self) { + delegate!(self, renderer, renderer.start_transformation()); + } + + fn end_transformation(&mut self, transformation: Transformation) { + delegate!(self, renderer, renderer.end_transformation(transformation)); + } +} + +impl<L, R> core::text::Renderer for Renderer<L, R> +where + L: core::text::Renderer, + R: core::text::Renderer< + Font = L::Font, + Paragraph = L::Paragraph, + Editor = L::Editor, + >, +{ + type Font = L::Font; + type Paragraph = L::Paragraph; + type Editor = L::Editor; + + const ICON_FONT: Self::Font = L::ICON_FONT; + const CHECKMARK_ICON: char = L::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = L::ARROW_DOWN_ICON; + + fn default_font(&self) -> Self::Font { + delegate!(self, renderer, renderer.default_font()) + } + + fn default_size(&self) -> core::Pixels { + delegate!(self, renderer, renderer.default_size()) + } + + fn load_font(&mut self, font: std::borrow::Cow<'static, [u8]>) { + delegate!(self, renderer, renderer.load_font(font)); + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.fill_paragraph(text, position, color, clip_bounds) + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.fill_editor(editor, position, color, clip_bounds) + ); + } + + fn fill_text( + &mut self, + text: core::Text<'_, Self::Font>, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.fill_text(text, position, color, clip_bounds) + ); + } +} + +impl<L, R> image::Renderer for Renderer<L, R> +where + L: image::Renderer, + R: image::Renderer<Handle = L::Handle>, +{ + type Handle = L::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size<u32> { + delegate!(self, renderer, renderer.measure_image(handle)) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.draw_image(handle, filter_method, bounds) + ); + } +} + +impl<L, R> svg::Renderer for Renderer<L, R> +where + L: svg::Renderer, + R: svg::Renderer, +{ + fn measure_svg(&self, handle: &svg::Handle) -> Size<u32> { + delegate!(self, renderer, renderer.measure_svg(handle)) + } + + fn draw_svg( + &mut self, + handle: svg::Handle, + color: Option<Color>, + bounds: Rectangle, + ) { + delegate!(self, renderer, renderer.draw_svg(handle, color, bounds)); + } +} + +impl<L, R> mesh::Renderer for Renderer<L, R> +where + L: mesh::Renderer, + R: mesh::Renderer, +{ + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + delegate!(self, renderer, renderer.draw_mesh(mesh)); + } +} + +pub enum Compositor<L, R> +where + L: graphics::Compositor, + R: graphics::Compositor, +{ + Left(L), + Right(R), +} + +pub enum Surface<L, R> { + Left(L), + Right(R), +} + +impl<L, R> graphics::Compositor for Compositor<L, R> +where + L: graphics::Compositor, + R: graphics::Compositor, +{ + type Renderer = Renderer<L::Renderer, R::Renderer>; + type Surface = Surface<L::Surface, R::Surface>; + + async fn with_backend<W: compositor::Window + Clone>( + settings: graphics::Settings, + compatible_window: W, + backend: Option<&str>, + ) -> Result<Self, graphics::Error> { + use std::env; + + let backends = backend + .map(str::to_owned) + .or_else(|| env::var("ICED_BACKEND").ok()); + + let mut candidates: Vec<_> = backends + .map(|backends| { + backends + .split(',') + .filter(|candidate| !candidate.is_empty()) + .map(str::to_owned) + .map(Some) + .collect() + }) + .unwrap_or_default(); + + if candidates.is_empty() { + candidates.push(None); + } + + let mut errors = vec![]; + + for backend in candidates.iter().map(Option::as_deref) { + match L::with_backend(settings, compatible_window.clone(), backend) + .await + { + Ok(compositor) => return Ok(Self::Left(compositor)), + Err(error) => { + errors.push(error); + } + } + + match R::with_backend(settings, compatible_window.clone(), backend) + .await + { + Ok(compositor) => return Ok(Self::Right(compositor)), + Err(error) => { + errors.push(error); + } + } + } + + Err(graphics::Error::List(errors)) + } + + fn create_renderer(&self) -> Self::Renderer { + match self { + Self::Left(compositor) => { + Renderer::Left(compositor.create_renderer()) + } + Self::Right(compositor) => { + Renderer::Right(compositor.create_renderer()) + } + } + } + + fn create_surface<W: compositor::Window + Clone>( + &mut self, + window: W, + width: u32, + height: u32, + ) -> Self::Surface { + match self { + Self::Left(compositor) => { + Surface::Left(compositor.create_surface(window, width, height)) + } + Self::Right(compositor) => { + Surface::Right(compositor.create_surface(window, width, height)) + } + } + } + + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ) { + match (self, surface) { + (Self::Left(compositor), Surface::Left(surface)) => { + compositor.configure_surface(surface, width, height); + } + (Self::Right(compositor), Surface::Right(surface)) => { + compositor.configure_surface(surface, width, height); + } + _ => unreachable!(), + } + } + + fn fetch_information(&self) -> compositor::Information { + delegate!(self, compositor, compositor.fetch_information()) + } + + fn present<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &graphics::Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), compositor::SurfaceError> { + match (self, renderer, surface) { + ( + Self::Left(compositor), + Renderer::Left(renderer), + Surface::Left(surface), + ) => compositor.present( + renderer, + surface, + viewport, + background_color, + overlay, + ), + ( + Self::Right(compositor), + Renderer::Right(renderer), + Surface::Right(surface), + ) => compositor.present( + renderer, + surface, + viewport, + background_color, + overlay, + ), + _ => unreachable!(), + } + } + + fn screenshot<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &graphics::Viewport, + background_color: Color, + overlay: &[T], + ) -> Vec<u8> { + match (self, renderer, surface) { + ( + Self::Left(compositor), + Renderer::Left(renderer), + Surface::Left(surface), + ) => compositor.screenshot( + renderer, + surface, + viewport, + background_color, + overlay, + ), + ( + Self::Right(compositor), + Renderer::Right(renderer), + Surface::Right(surface), + ) => compositor.screenshot( + renderer, + surface, + viewport, + background_color, + overlay, + ), + _ => unreachable!(), + } + } +} + +#[cfg(feature = "wgpu")] +impl<L, R> iced_wgpu::primitive::pipeline::Renderer for Renderer<L, R> +where + L: iced_wgpu::primitive::pipeline::Renderer, + R: core::Renderer, +{ + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl iced_wgpu::primitive::pipeline::Primitive, + ) { + match self { + Self::Left(renderer) => { + renderer.draw_pipeline_primitive(bounds, primitive); + } + Self::Right(_) => { + log::warn!( + "Custom shader primitive is not supported with this renderer." + ); + } + } + } +} + +#[cfg(feature = "geometry")] +mod geometry { + use super::Renderer; + use crate::core::{Point, Radians, Size, Vector}; + use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; + use crate::graphics::Cached; + + impl<L, R> geometry::Renderer for Renderer<L, R> + where + L: geometry::Renderer, + R: geometry::Renderer, + { + type Geometry = Geometry<L::Geometry, R::Geometry>; + type Frame = Frame<L::Frame, R::Frame>; + + fn new_frame(&self, size: iced_graphics::core::Size) -> Self::Frame { + match self { + Self::Left(renderer) => Frame::Left(renderer.new_frame(size)), + Self::Right(renderer) => Frame::Right(renderer.new_frame(size)), + } + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + match (self, geometry) { + (Self::Left(renderer), Geometry::Left(geometry)) => { + renderer.draw_geometry(geometry); + } + (Self::Right(renderer), Geometry::Right(geometry)) => { + renderer.draw_geometry(geometry); + } + _ => unreachable!(), + } + } + } + + pub enum Geometry<L, R> { + Left(L), + Right(R), + } + + impl<L, R> Cached for Geometry<L, R> + where + L: Cached, + R: Cached, + { + type Cache = Geometry<L::Cache, R::Cache>; + + fn load(cache: &Self::Cache) -> Self { + match cache { + Geometry::Left(cache) => Self::Left(L::load(cache)), + Geometry::Right(cache) => Self::Right(R::load(cache)), + } + } + + fn cache(self) -> Self::Cache { + match self { + Self::Left(geometry) => Geometry::Left(geometry.cache()), + Self::Right(geometry) => Geometry::Right(geometry.cache()), + } + } + } + + pub enum Frame<L, R> { + Left(L), + Right(R), + } + + impl<L, R> geometry::frame::Backend for Frame<L, R> + where + L: geometry::frame::Backend, + R: geometry::frame::Backend, + { + type Geometry = Geometry<L::Geometry, R::Geometry>; + + fn width(&self) -> f32 { + delegate!(self, frame, frame.width()) + } + + fn height(&self) -> f32 { + delegate!(self, frame, frame.height()) + } + + fn size(&self) -> Size { + delegate!(self, frame, frame.size()) + } + + fn center(&self) -> Point { + delegate!(self, frame, frame.center()) + } + + fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + delegate!(self, frame, frame.fill(path, fill)); + } + + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into<Fill>, + ) { + delegate!(self, frame, frame.fill_rectangle(top_left, size, fill)); + } + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + delegate!(self, frame, frame.stroke(path, stroke)); + } + + fn fill_text(&mut self, text: impl Into<Text>) { + delegate!(self, frame, frame.fill_text(text)); + } + + fn push_transform(&mut self) { + delegate!(self, frame, frame.push_transform()); + } + + fn pop_transform(&mut self) { + delegate!(self, frame, frame.pop_transform()); + } + + fn draft(&mut self, size: Size) -> Self { + match self { + Self::Left(frame) => Self::Left(frame.draft(size)), + Self::Right(frame) => Self::Right(frame.draft(size)), + } + } + + fn paste(&mut self, frame: Self, at: Point) { + match (self, frame) { + (Self::Left(target), Self::Left(source)) => { + target.paste(source, at); + } + (Self::Right(target), Self::Right(source)) => { + target.paste(source, at); + } + _ => unreachable!(), + } + } + + fn translate(&mut self, translation: Vector) { + delegate!(self, frame, frame.translate(translation)); + } + + fn rotate(&mut self, angle: impl Into<Radians>) { + delegate!(self, frame, frame.rotate(angle)); + } + + fn scale(&mut self, scale: impl Into<f32>) { + delegate!(self, frame, frame.scale(scale)); + } + + fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + delegate!(self, frame, frame.scale_nonuniform(scale)); + } + + fn into_geometry(self) -> Self::Geometry { + match self { + Frame::Left(frame) => Geometry::Left(frame.into_geometry()), + Frame::Right(frame) => Geometry::Right(frame.into_geometry()), + } + } + } +} + +impl<L, R> compositor::Default for Renderer<L, R> +where + L: compositor::Default, + R: compositor::Default, +{ + type Compositor = Compositor<L::Compositor, R::Compositor>; +} diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs deleted file mode 100644 index 36435148..00000000 --- a/renderer/src/geometry.rs +++ /dev/null @@ -1,210 +0,0 @@ -mod cache; - -pub use cache::Cache; - -use crate::core::{Point, Radians, Rectangle, Size, Transformation, Vector}; -use crate::graphics::geometry::{Fill, Path, Stroke, Text}; -use crate::Renderer; - -macro_rules! delegate { - ($frame:expr, $name:ident, $body:expr) => { - match $frame { - Self::TinySkia($name) => $body, - #[cfg(feature = "wgpu")] - Self::Wgpu($name) => $body, - } - }; -} - -pub enum Geometry { - TinySkia(iced_tiny_skia::Primitive), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::Primitive), -} - -impl Geometry { - pub fn transform(self, transformation: Transformation) -> Self { - match self { - Self::TinySkia(primitive) => { - Self::TinySkia(primitive.transform(transformation)) - } - #[cfg(feature = "wgpu")] - Self::Wgpu(primitive) => { - Self::Wgpu(primitive.transform(transformation)) - } - } - } -} - -pub enum Frame { - TinySkia(iced_tiny_skia::geometry::Frame), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::geometry::Frame), -} - -impl Frame { - pub fn new(renderer: &Renderer, size: Size) -> Self { - match renderer { - Renderer::TinySkia(_) => { - Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) - } - #[cfg(feature = "wgpu")] - Renderer::Wgpu(_) => { - Frame::Wgpu(iced_wgpu::geometry::Frame::new(size)) - } - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - delegate!(self, frame, frame.width()) - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - delegate!(self, frame, frame.height()) - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - delegate!(self, frame, frame.size()) - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - delegate!(self, frame, frame.center()) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { - delegate!(self, frame, frame.fill(path, fill)); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into<Fill>, - ) { - delegate!(self, frame, frame.fill_rectangle(top_left, size, fill)); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { - delegate!(self, frame, frame.stroke(path, stroke)); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a `Canvas`. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - pub fn fill_text(&mut self, text: impl Into<Text>) { - delegate!(self, frame, frame.fill_text(text)); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R { - delegate!(self, frame, frame.push_transform()); - - let result = f(self); - - delegate!(self, frame, frame.pop_transform()); - - result - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip<R>( - &mut self, - region: Rectangle, - f: impl FnOnce(&mut Frame) -> R, - ) -> R { - let mut frame = match self { - Self::TinySkia(_) => Self::TinySkia( - iced_tiny_skia::geometry::Frame::new(region.size()), - ), - #[cfg(feature = "wgpu")] - Self::Wgpu(_) => { - Self::Wgpu(iced_wgpu::geometry::Frame::new(region.size())) - } - }; - - let result = f(&mut frame); - - let origin = Point::new(region.x, region.y); - - match (self, frame) { - (Self::TinySkia(target), Self::TinySkia(frame)) => { - target.clip(frame, origin); - } - #[cfg(feature = "wgpu")] - (Self::Wgpu(target), Self::Wgpu(frame)) => { - target.clip(frame, origin); - } - #[allow(unreachable_patterns)] - _ => unreachable!(), - }; - - result - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - delegate!(self, frame, frame.translate(translation)); - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: impl Into<Radians>) { - delegate!(self, frame, frame.rotate(angle)); - } - - /// Applies a uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: impl Into<f32>) { - delegate!(self, frame, frame.scale(scale)); - } - - /// Applies a non-uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { - delegate!(self, frame, frame.scale_nonuniform(scale)); - } - - pub fn into_geometry(self) -> Geometry { - match self { - Self::TinySkia(frame) => Geometry::TinySkia(frame.into_primitive()), - #[cfg(feature = "wgpu")] - Self::Wgpu(frame) => Geometry::Wgpu(frame.into_primitive()), - } - } -} diff --git a/renderer/src/geometry/cache.rs b/renderer/src/geometry/cache.rs deleted file mode 100644 index 3aff76b9..00000000 --- a/renderer/src/geometry/cache.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::core::Size; -use crate::geometry::{Frame, Geometry}; -use crate::Renderer; - -use std::cell::RefCell; -use std::sync::Arc; - -/// 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. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell<State>, -} - -#[derive(Debug, Default)] -enum State { - #[default] - Empty, - Filled { - bounds: Size, - primitive: Internal, - }, -} - -#[derive(Debug, Clone)] -enum Internal { - TinySkia(Arc<iced_tiny_skia::Primitive>), - #[cfg(feature = "wgpu")] - Wgpu(Arc<iced_wgpu::Primitive>), -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: RefCell::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - renderer: &Renderer, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - match primitive { - Internal::TinySkia(primitive) => { - return Geometry::TinySkia( - iced_tiny_skia::Primitive::Cache { - content: primitive.clone(), - }, - ); - } - #[cfg(feature = "wgpu")] - Internal::Wgpu(primitive) => { - return Geometry::Wgpu(iced_wgpu::Primitive::Cache { - content: primitive.clone(), - }); - } - } - } - } - - let mut frame = Frame::new(renderer, bounds); - draw_fn(&mut frame); - - let primitive = { - let geometry = frame.into_geometry(); - - match geometry { - Geometry::TinySkia(primitive) => { - Internal::TinySkia(Arc::new(primitive)) - } - #[cfg(feature = "wgpu")] - Geometry::Wgpu(primitive) => { - Internal::Wgpu(Arc::new(primitive)) - } - } - }; - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - match primitive { - Internal::TinySkia(primitive) => { - Geometry::TinySkia(iced_tiny_skia::Primitive::Cache { - content: primitive, - }) - } - #[cfg(feature = "wgpu")] - Internal::Wgpu(primitive) => { - Geometry::Wgpu(iced_wgpu::Primitive::Cache { - content: primitive, - }) - } - } - } -} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 757c264d..7c48995d 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -4,299 +4,51 @@ #[cfg(feature = "wgpu")] pub use iced_wgpu as wgpu; -pub mod compositor; - -#[cfg(feature = "geometry")] -pub mod geometry; - -mod settings; +pub mod fallback; pub use iced_graphics as graphics; pub use iced_graphics::core; -pub use compositor::Compositor; -pub use settings::Settings; - #[cfg(feature = "geometry")] -pub use geometry::Geometry; - -use crate::core::renderer; -use crate::core::text::{self, Text}; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, -}; -use crate::graphics::text::Editor; -use crate::graphics::text::Paragraph; -use crate::graphics::Mesh; - -use std::borrow::Cow; +pub use iced_graphics::geometry; /// The default graphics renderer for [`iced`]. /// /// [`iced`]: https://github.com/iced-rs/iced -pub enum Renderer { - TinySkia(iced_tiny_skia::Renderer), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::Renderer), -} - -macro_rules! delegate { - ($renderer:expr, $name:ident, $body:expr) => { - match $renderer { - Self::TinySkia($name) => $body, - #[cfg(feature = "wgpu")] - Self::Wgpu($name) => $body, - } - }; -} - -impl Renderer { - pub fn draw_mesh(&mut self, mesh: Mesh) { - match self { - Self::TinySkia(_) => { - log::warn!("Unsupported mesh primitive: {mesh:?}"); - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - renderer.draw_primitive(iced_wgpu::Primitive::Custom( - iced_wgpu::primitive::Custom::Mesh(mesh), - )); - } - } - } -} - -impl core::Renderer for Renderer { - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - match self { - Self::TinySkia(renderer) => { - let primitives = renderer.start_layer(); - - f(self); - - match self { - Self::TinySkia(renderer) => { - renderer.end_layer(primitives, bounds); - } - #[cfg(feature = "wgpu")] - _ => unreachable!(), - } - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - let primitives = renderer.start_layer(); - - f(self); - - match self { - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - renderer.end_layer(primitives, bounds); - } - _ => unreachable!(), - } - } - } - } - - fn with_transformation( - &mut self, - transformation: Transformation, - f: impl FnOnce(&mut Self), - ) { - match self { - Self::TinySkia(renderer) => { - let primitives = renderer.start_transformation(); - - f(self); - - match self { - Self::TinySkia(renderer) => { - renderer.end_transformation(primitives, transformation); - } - #[cfg(feature = "wgpu")] - _ => unreachable!(), - } - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - let primitives = renderer.start_transformation(); - - f(self); +pub type Renderer = renderer::Renderer; - match self { - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - renderer.end_transformation(primitives, transformation); - } - _ => unreachable!(), - } - } - } - } - - fn fill_quad( - &mut self, - quad: renderer::Quad, - background: impl Into<Background>, - ) { - delegate!(self, renderer, renderer.fill_quad(quad, background)); - } - - fn clear(&mut self) { - delegate!(self, renderer, renderer.clear()); - } -} - -impl text::Renderer for Renderer { - type Font = Font; - type Paragraph = Paragraph; - type Editor = Editor; - - const ICON_FONT: Font = iced_tiny_skia::Renderer::ICON_FONT; - const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::CHECKMARK_ICON; - const ARROW_DOWN_ICON: char = iced_tiny_skia::Renderer::ARROW_DOWN_ICON; - - fn default_font(&self) -> Self::Font { - delegate!(self, renderer, renderer.default_font()) - } - - fn default_size(&self) -> Pixels { - delegate!(self, renderer, renderer.default_size()) - } - - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - delegate!(self, renderer, renderer.load_font(bytes)); - } - - fn fill_paragraph( - &mut self, - paragraph: &Self::Paragraph, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - delegate!( - self, - renderer, - renderer.fill_paragraph(paragraph, position, color, clip_bounds) - ); - } - - fn fill_editor( - &mut self, - editor: &Self::Editor, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - delegate!( - self, - renderer, - renderer.fill_editor(editor, position, color, clip_bounds) - ); - } - - fn fill_text( - &mut self, - text: Text<'_, Self::Font>, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - delegate!( - self, - renderer, - renderer.fill_text(text, position, color, clip_bounds) - ); - } -} - -#[cfg(feature = "image")] -impl crate::core::image::Renderer for Renderer { - type Handle = crate::core::image::Handle; +/// The default graphics compositor for [`iced`]. +/// +/// [`iced`]: https://github.com/iced-rs/iced +pub type Compositor = renderer::Compositor; - fn dimensions( - &self, - handle: &crate::core::image::Handle, - ) -> core::Size<u32> { - delegate!(self, renderer, renderer.dimensions(handle)) - } +#[cfg(all(feature = "wgpu", feature = "tiny-skia"))] +mod renderer { + pub type Renderer = crate::fallback::Renderer< + iced_wgpu::Renderer, + iced_tiny_skia::Renderer, + >; - fn draw( - &mut self, - handle: crate::core::image::Handle, - filter_method: crate::core::image::FilterMethod, - bounds: Rectangle, - ) { - delegate!(self, renderer, renderer.draw(handle, filter_method, bounds)); - } + pub type Compositor = crate::fallback::Compositor< + iced_wgpu::window::Compositor, + iced_tiny_skia::window::Compositor, + >; } -#[cfg(feature = "svg")] -impl crate::core::svg::Renderer for Renderer { - fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size<u32> { - delegate!(self, renderer, renderer.dimensions(handle)) - } - - fn draw( - &mut self, - handle: crate::core::svg::Handle, - color: Option<crate::core::Color>, - bounds: Rectangle, - ) { - delegate!(self, renderer, renderer.draw(handle, color, bounds)); - } +#[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))] +mod renderer { + pub type Renderer = iced_wgpu::Renderer; + pub type Compositor = iced_wgpu::window::Compositor; } -#[cfg(feature = "geometry")] -impl crate::graphics::geometry::Renderer for Renderer { - type Geometry = crate::Geometry; - - fn draw(&mut self, layers: Vec<Self::Geometry>) { - match self { - Self::TinySkia(renderer) => { - for layer in layers { - match layer { - crate::Geometry::TinySkia(primitive) => { - renderer.draw_primitive(primitive); - } - #[cfg(feature = "wgpu")] - crate::Geometry::Wgpu(_) => unreachable!(), - } - } - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - for layer in layers { - match layer { - crate::Geometry::Wgpu(primitive) => { - renderer.draw_primitive(primitive); - } - crate::Geometry::TinySkia(_) => unreachable!(), - } - } - } - } - } +#[cfg(all(not(feature = "wgpu"), feature = "tiny-skia"))] +mod renderer { + pub type Renderer = iced_tiny_skia::Renderer; + pub type Compositor = iced_tiny_skia::window::Compositor; } -#[cfg(feature = "wgpu")] -impl iced_wgpu::primitive::pipeline::Renderer for Renderer { - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl wgpu::primitive::pipeline::Primitive, - ) { - match self { - Self::TinySkia(_renderer) => { - log::warn!( - "Custom shader primitive is unavailable with tiny-skia." - ); - } - Self::Wgpu(renderer) => { - renderer.draw_pipeline_primitive(bounds, primitive); - } - } - } +#[cfg(not(any(feature = "wgpu", feature = "tiny-skia")))] +mod renderer { + pub type Renderer = (); + pub type Compositor = (); } diff --git a/runtime/src/program.rs b/runtime/src/program.rs index 6c1b8f07..0ea94d3b 100644 --- a/runtime/src/program.rs +++ b/runtime/src/program.rs @@ -2,7 +2,7 @@ use crate::Command; use iced_core::text; -use iced_core::{Element, Renderer}; +use iced_core::Element; mod state; @@ -11,7 +11,7 @@ pub use state::State; /// The core of a user interface application following The Elm Architecture. pub trait Program: Sized { /// The graphics backend to use to draw the [`Program`]. - type Renderer: Renderer + text::Renderer; + type Renderer: text::Renderer; /// The theme used to draw the [`Program`]. type Theme; diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 748fb651..006225ed 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -45,7 +45,7 @@ where /// /// ```no_run /// # mod iced_wgpu { - /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Renderer = (); /// # } /// # /// # pub struct Counter; @@ -62,7 +62,7 @@ where /// // Initialization /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); - /// let mut renderer = Renderer::new(); + /// let mut renderer = Renderer::default(); /// let mut window_size = Size::new(1024.0, 768.0); /// /// // Application loop @@ -121,7 +121,7 @@ where /// /// ```no_run /// # mod iced_wgpu { - /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Renderer = (); /// # } /// # /// # pub struct Counter; @@ -139,7 +139,7 @@ where /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); - /// let mut renderer = Renderer::new(); + /// let mut renderer = Renderer::default(); /// let mut window_size = Size::new(1024.0, 768.0); /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; @@ -374,7 +374,7 @@ where /// /// ```no_run /// # mod iced_wgpu { - /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Renderer = (); /// # pub type Theme = (); /// # } /// # @@ -394,7 +394,7 @@ where /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); - /// let mut renderer = Renderer::new(); + /// let mut renderer = Renderer::default(); /// let mut window_size = Size::new(1024.0, 768.0); /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; diff --git a/src/application.rs b/src/application.rs index 8317abcb..9197834b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,6 @@ //! Build interactive cross-platform applications. +use crate::core::text; +use crate::graphics::compositor; use crate::shell::application; use crate::{Command, Element, Executor, Settings, Subscription}; @@ -60,7 +62,7 @@ pub use application::{Appearance, DefaultStyle}; /// ```no_run /// use iced::advanced::Application; /// use iced::executor; -/// use iced::{Command, Element, Settings, Theme}; +/// use iced::{Command, Element, Settings, Theme, Renderer}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -73,6 +75,7 @@ pub use application::{Appearance, DefaultStyle}; /// type Flags = (); /// type Message = (); /// type Theme = Theme; +/// type Renderer = Renderer; /// /// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { /// (Hello, Command::none()) @@ -109,6 +112,9 @@ where /// The theme of your [`Application`]. type Theme: Default; + /// The renderer of your [`Application`]. + type Renderer: text::Renderer + compositor::Default; + /// The data needed to initialize your [`Application`]. type Flags; @@ -142,7 +148,7 @@ where /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; + fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; /// Returns the current [`Theme`] of the [`Application`]. /// @@ -195,7 +201,7 @@ where Self: 'static, { #[allow(clippy::needless_update)] - let renderer_settings = crate::renderer::Settings { + let renderer_settings = crate::graphics::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { @@ -203,13 +209,13 @@ where } else { None }, - ..crate::renderer::Settings::default() + ..crate::graphics::Settings::default() }; let run = crate::shell::application::run::< Instance<Self>, Self::Executor, - crate::renderer::Compositor, + <Self::Renderer as compositor::Default>::Compositor, >(settings.into(), renderer_settings); #[cfg(target_arch = "wasm32")] @@ -241,7 +247,7 @@ where { type Message = A::Message; type Theme = A::Theme; - type Renderer = crate::Renderer; + type Renderer = A::Renderer; fn update(&mut self, message: Self::Message) -> Command<Self::Message> { self.0.update(message) @@ -372,15 +372,16 @@ pub type Result = std::result::Result<(), Error>; /// ] /// } /// ``` -pub fn run<State, Message, Theme>( +pub fn run<State, Message, Theme, Renderer>( title: impl program::Title<State> + 'static, update: impl program::Update<State, Message> + 'static, - view: impl for<'a> program::View<'a, State, Message, Theme> + 'static, + view: impl for<'a> program::View<'a, State, Message, Theme, Renderer> + 'static, ) -> Result where State: Default + 'static, Message: std::fmt::Debug + Send + 'static, Theme: Default + program::DefaultStyle + 'static, + Renderer: program::Renderer + 'static, { program(title, update, view).run() } diff --git a/src/multi_window.rs b/src/multi_window.rs index fca0be46..b81297dc 100644 --- a/src/multi_window.rs +++ b/src/multi_window.rs @@ -174,7 +174,7 @@ where Self: 'static, { #[allow(clippy::needless_update)] - let renderer_settings = crate::renderer::Settings { + let renderer_settings = crate::graphics::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { @@ -182,7 +182,7 @@ where } else { None }, - ..crate::renderer::Settings::default() + ..crate::graphics::Settings::default() }; Ok(crate::shell::multi_window::run::< diff --git a/src/program.rs b/src/program.rs index 7a366585..d4c2a266 100644 --- a/src/program.rs +++ b/src/program.rs @@ -31,7 +31,9 @@ //! } //! ``` use crate::application::Application; +use crate::core::text; use crate::executor::{self, Executor}; +use crate::graphics::compositor; use crate::window; use crate::{Command, Element, Font, Result, Settings, Size, Subscription}; @@ -67,37 +69,41 @@ use std::borrow::Cow; /// ] /// } /// ``` -pub fn program<State, Message, Theme>( +pub fn program<State, Message, Theme, Renderer>( title: impl Title<State>, update: impl Update<State, Message>, - view: impl for<'a> self::View<'a, State, Message, Theme>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, ) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> where State: 'static, Message: Send + std::fmt::Debug, Theme: Default + DefaultStyle, + Renderer: self::Renderer, { use std::marker::PhantomData; - struct Application<State, Message, Theme, Update, View> { + struct Application<State, Message, Theme, Renderer, Update, View> { update: Update, view: View, _state: PhantomData<State>, _message: PhantomData<Message>, _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, } - impl<State, Message, Theme, Update, View> Definition - for Application<State, Message, Theme, Update, View> + impl<State, Message, Theme, Renderer, Update, View> Definition + for Application<State, Message, Theme, Renderer, Update, View> where Message: Send + std::fmt::Debug, Theme: Default + DefaultStyle, + Renderer: self::Renderer, Update: self::Update<State, Message>, - View: for<'a> self::View<'a, State, Message, Theme>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, { type State = State; type Message = Message; type Theme = Theme; + type Renderer = Renderer; type Executor = executor::Default; fn load(&self) -> Command<Self::Message> { @@ -115,7 +121,7 @@ where fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme> { + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.view.view(state).into() } } @@ -127,6 +133,7 @@ where _state: PhantomData, _message: PhantomData, _theme: PhantomData, + _renderer: PhantomData, }, settings: Settings::default(), } @@ -184,6 +191,7 @@ impl<P: Definition> Program<P> { impl<P: Definition, I: Fn() -> P::State> Application for Instance<P, I> { type Message = P::Message; type Theme = P::Theme; + type Renderer = P::Renderer; type Flags = (P, I); type Executor = P::Executor; @@ -216,7 +224,7 @@ impl<P: Definition> Program<P> { fn view( &self, - ) -> crate::Element<'_, Self::Message, Self::Theme, crate::Renderer> + ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> { self.program.view(&self.state) } @@ -417,6 +425,9 @@ pub trait Definition: Sized { /// The theme of the program. type Theme: Default + DefaultStyle; + /// The renderer of the program. + type Renderer: Renderer; + /// The executor of the program. type Executor: Executor; @@ -431,7 +442,7 @@ pub trait Definition: Sized { fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme>; + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; fn title(&self, _state: &Self::State) -> String { String::from("A cool iced application!") @@ -470,6 +481,7 @@ fn with_title<P: Definition>( type State = P::State; type Message = P::Message; type Theme = P::Theme; + type Renderer = P::Renderer; type Executor = P::Executor; fn load(&self) -> Command<Self::Message> { @@ -491,7 +503,7 @@ fn with_title<P: Definition>( fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme> { + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.program.view(state) } @@ -534,6 +546,7 @@ fn with_load<P: Definition>( type State = P::State; type Message = P::Message; type Theme = P::Theme; + type Renderer = P::Renderer; type Executor = executor::Default; fn load(&self) -> Command<Self::Message> { @@ -551,7 +564,7 @@ fn with_load<P: Definition>( fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme> { + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.program.view(state) } @@ -598,6 +611,7 @@ fn with_subscription<P: Definition>( type State = P::State; type Message = P::Message; type Theme = P::Theme; + type Renderer = P::Renderer; type Executor = executor::Default; fn subscription( @@ -622,7 +636,7 @@ fn with_subscription<P: Definition>( fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme> { + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.program.view(state) } @@ -665,6 +679,7 @@ fn with_theme<P: Definition>( type State = P::State; type Message = P::Message; type Theme = P::Theme; + type Renderer = P::Renderer; type Executor = P::Executor; fn theme(&self, state: &Self::State) -> Self::Theme { @@ -690,7 +705,7 @@ fn with_theme<P: Definition>( fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme> { + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.program.view(state) } @@ -729,6 +744,7 @@ fn with_style<P: Definition>( type State = P::State; type Message = P::Message; type Theme = P::Theme; + type Renderer = P::Renderer; type Executor = P::Executor; fn style( @@ -758,7 +774,7 @@ fn with_style<P: Definition>( fn view<'a>( &self, state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme> { + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.program.view(state) } @@ -834,18 +850,30 @@ where /// /// This trait allows the [`program`] builder to take any closure that /// returns any `Into<Element<'_, Message>>`. -pub trait View<'a, State, Message, Theme> { +pub trait View<'a, State, Message, Theme, Renderer> { /// Produces the widget of the [`Program`]. - fn view(&self, state: &'a State) -> impl Into<Element<'a, Message, Theme>>; + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; } -impl<'a, T, State, Message, Theme, Widget> View<'a, State, Message, Theme> for T +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T where T: Fn(&'a State) -> Widget, State: 'static, - Widget: Into<Element<'a, Message, Theme>>, + Widget: Into<Element<'a, Message, Theme, Renderer>>, { - fn view(&self, state: &'a State) -> impl Into<Element<'a, Message, Theme>> { + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { self(state) } } + +/// The renderer of some [`Program`]. +pub trait Renderer: text::Renderer + compositor::Default {} + +impl<T> Renderer for T where T: text::Renderer + compositor::Default {} diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index b6487b38..8c8781e3 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -5,6 +5,7 @@ use crate::graphics::backend; use crate::graphics::text; use crate::graphics::{Damage, Viewport}; use crate::primitive::{self, Primitive}; +use crate::window; use std::borrow::Cow; @@ -989,8 +990,9 @@ fn rounded_box_sdf( (x.powf(2.0) + y.powf(2.0)).sqrt() - radius } -impl iced_graphics::Backend for Backend { +impl backend::Backend for Backend { type Primitive = primitive::Custom; + type Compositor = window::Compositor; } impl backend::Text for Backend { @@ -1018,3 +1020,12 @@ impl backend::Svg for Backend { self.vector_pipeline.viewport_dimensions(handle) } } + +#[cfg(feature = "geometry")] +impl crate::graphics::geometry::Backend for Backend { + type Frame = crate::geometry::Frame; + + fn new_frame(&self, size: Size) -> Self::Frame { + crate::geometry::Frame::new(size) + } +} diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 16787f89..76482e12 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -4,7 +4,7 @@ use crate::core::{ }; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; -use crate::graphics::geometry::{Path, Style, Text}; +use crate::graphics::geometry::{self, Path, Style, Text}; use crate::graphics::Gradient; use crate::primitive::{self, Primitive}; @@ -25,23 +25,36 @@ impl Frame { } } - pub fn width(&self) -> f32 { + pub fn into_primitive(self) -> Primitive { + Primitive::Clip { + bounds: Rectangle::new(Point::ORIGIN, self.size), + content: Box::new(Primitive::Group { + primitives: self.primitives, + }), + } + } +} + +impl geometry::frame::Backend for Frame { + type Geometry = Primitive; + + fn width(&self) -> f32 { self.size.width } - pub fn height(&self) -> f32 { + fn height(&self) -> f32 { self.size.height } - pub fn size(&self) -> Size { + fn size(&self) -> Size { self.size } - pub fn center(&self) -> Point { + fn center(&self) -> Point { Point::new(self.size.width / 2.0, self.size.height / 2.0) } - pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { let Some(path) = convert_path(path).and_then(|path| path.transform(self.transform)) else { @@ -61,7 +74,7 @@ impl Frame { })); } - pub fn fill_rectangle( + fn fill_rectangle( &mut self, top_left: Point, size: Size, @@ -89,7 +102,7 @@ impl Frame { })); } - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { let Some(path) = convert_path(path).and_then(|path| path.transform(self.transform)) else { @@ -110,7 +123,7 @@ impl Frame { })); } - pub fn fill_text(&mut self, text: impl Into<Text>) { + fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); let (scale_x, scale_y) = self.transform.get_scale(); @@ -174,51 +187,50 @@ impl Frame { } } - pub fn push_transform(&mut self) { + fn push_transform(&mut self) { self.stack.push(self.transform); } - pub fn pop_transform(&mut self) { + fn pop_transform(&mut self) { self.transform = self.stack.pop().expect("Pop transform"); } - pub fn clip(&mut self, frame: Self, at: Point) { + fn draft(&mut self, size: Size) -> Self { + Self::new(size) + } + + fn paste(&mut self, frame: Self, at: Point) { self.primitives.push(Primitive::Transform { transformation: Transformation::translate(at.x, at.y), content: Box::new(frame.into_primitive()), }); } - pub fn translate(&mut self, translation: Vector) { + fn translate(&mut self, translation: Vector) { self.transform = self.transform.pre_translate(translation.x, translation.y); } - pub fn rotate(&mut self, angle: impl Into<Radians>) { + fn rotate(&mut self, angle: impl Into<Radians>) { self.transform = self.transform.pre_concat( tiny_skia::Transform::from_rotate(angle.into().0.to_degrees()), ); } - pub fn scale(&mut self, scale: impl Into<f32>) { + fn scale(&mut self, scale: impl Into<f32>) { let scale = scale.into(); self.scale_nonuniform(Vector { x: scale, y: scale }); } - pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { let scale = scale.into(); self.transform = self.transform.pre_scale(scale.x, scale.y); } - pub fn into_primitive(self) -> Primitive { - Primitive::Clip { - bounds: Rectangle::new(Point::ORIGIN, self.size), - content: Box::new(Primitive::Group { - primitives: self.primitives, - }), - } + fn into_geometry(self) -> Self::Geometry { + self.into_primitive() } } diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index 7718d542..b7c428e4 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -1,5 +1,5 @@ use crate::core::Rectangle; -use crate::graphics::Damage; +use crate::graphics::{Damage, Mesh}; pub type Primitive = crate::graphics::Primitive<Custom>; @@ -42,3 +42,11 @@ impl Damage for Custom { } } } + +impl TryFrom<Mesh> for Custom { + type Error = &'static str; + + fn try_from(_mesh: Mesh) -> Result<Self, Self::Error> { + Err("unsupported") + } +} diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index ec27b218..01d015b4 100644 --- a/tiny_skia/src/settings.rs +++ b/tiny_skia/src/settings.rs @@ -1,4 +1,5 @@ use crate::core::{Font, Pixels}; +use crate::graphics; /// The settings of a [`Backend`]. /// @@ -22,3 +23,12 @@ impl Default for Settings { } } } + +impl From<graphics::Settings> for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + } + } +} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index a98825f1..25c57dc1 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,11 +1,11 @@ use crate::core::{Color, Rectangle, Size}; use crate::graphics::compositor::{self, Information}; use crate::graphics::damage; -use crate::graphics::{Error, Viewport}; +use crate::graphics::error::{self, Error}; +use crate::graphics::{self, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; use std::collections::VecDeque; -use std::future::{self, Future}; use std::num::NonZeroU32; pub struct Compositor { @@ -25,15 +25,25 @@ pub struct Surface { } impl crate::graphics::Compositor for Compositor { - type Settings = Settings; type Renderer = Renderer; type Surface = Surface; - fn new<W: compositor::Window>( - settings: Self::Settings, + async fn with_backend<W: compositor::Window>( + settings: graphics::Settings, compatible_window: W, - ) -> impl Future<Output = Result<Self, Error>> { - future::ready(Ok(new(settings, compatible_window))) + backend: Option<&str>, + ) -> Result<Self, Error> { + match backend { + None | Some("tiny-skia") | Some("tiny_skia") => { + Ok(new(settings.into(), compatible_window)) + } + Some(backend) => Err(Error::GraphicsAdapterNotFound { + backend: "tiny-skia", + reason: error::Reason::DidNotMatch { + preferred_backend: backend.to_owned(), + }, + }), + } } fn create_renderer(&self) -> Self::Renderer { diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 4a0d89f0..f6162e0f 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -32,6 +32,7 @@ glyphon.workspace = true guillotiere.workspace = true log.workspace = true once_cell.workspace = true +thiserror.workspace = true wgpu.workspace = true lyon.workspace = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 09ddbe4d..5019191c 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -7,6 +7,7 @@ use crate::primitive::{self, Primitive}; use crate::quad; use crate::text; use crate::triangle; +use crate::window; use crate::{Layer, Settings}; #[cfg(feature = "tracing")] @@ -371,8 +372,9 @@ impl Backend { } } -impl crate::graphics::Backend for Backend { +impl backend::Backend for Backend { type Primitive = primitive::Custom; + type Compositor = window::Compositor; } impl backend::Text for Backend { @@ -397,3 +399,12 @@ impl backend::Svg for Backend { self.image_pipeline.viewport_dimensions(handle) } } + +#[cfg(feature = "geometry")] +impl crate::graphics::geometry::Backend for Backend { + type Frame = crate::geometry::Frame; + + fn new_frame(&self, size: Size) -> Self::Frame { + crate::geometry::Frame::new(size) + } +} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index f4e0fbda..ba56c59d 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -6,7 +6,7 @@ use crate::core::{ use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; @@ -14,6 +14,7 @@ use crate::primitive::{self, Primitive}; use lyon::geom::euclid; use lyon::tessellation; + use std::borrow::Cow; /// A frame for drawing some geometry. @@ -27,191 +28,87 @@ pub struct Frame { stroke_tessellator: tessellation::StrokeTessellator, } -enum Buffer { - Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>), - Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>), -} - -struct BufferStack { - stack: Vec<Buffer>, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(_) => match self.stack.last() { - Some(Buffer::Gradient(_)) => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - )); - } +impl Frame { + /// Creates a new [`Frame`] with the given [`Size`]. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform(lyon::math::Transform::identity()), }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), } - - self.stack.last_mut().unwrap() } - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box<dyn tessellation::FillGeometryBuilder + 'a> { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color::pack(*color)), - )) - } - (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - GradientVertex2DBuilder { - gradient: gradient.pack(), - }, - )) + fn into_primitives(mut self) -> Vec<Primitive> { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::Custom( + primitive::Custom::Mesh(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }), + )); + } + } + Buffer::Gradient(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::Custom( + primitive::Custom::Mesh(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }), + )); + } + } } - _ => unreachable!(), } - } - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color::pack(*color)), - )) - } - (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - GradientVertex2DBuilder { - gradient: gradient.pack(), - }, - )) - } - _ => unreachable!(), - } + self.primitives } } -#[derive(Debug)] -struct Transforms { - previous: Vec<Transform>, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform(lyon::math::Transform); - -impl Transform { - fn is_identity(&self) -> bool { - self.0 == lyon::math::Transform::identity() - } - - fn is_scale_translation(&self) -> bool { - self.0.m12.abs() < 2.0 * f32::EPSILON - && self.0.m21.abs() < 2.0 * f32::EPSILON - } - - fn scale(&self) -> (f32, f32) { - (self.0.m11, self.0.m22) - } - - fn transform_point(&self, point: Point) -> Point { - let transformed = self - .0 - .transform_point(euclid::Point2D::new(point.x, point.y)); - - Point { - x: transformed.x, - y: transformed.y, - } - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - match &mut gradient { - Gradient::Linear(linear) => { - linear.start = self.transform_point(linear.start); - linear.end = self.transform_point(linear.end); - } - } - - gradient - } -} +impl geometry::frame::Backend for Frame { + type Geometry = Primitive; -impl Frame { /// Creates a new empty [`Frame`] with the given dimensions. /// /// The default coordinate system of a [`Frame`] has its origin at the /// top-left corner of its bounds. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform(lyon::math::Transform::identity()), - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - /// Returns the width of the [`Frame`]. #[inline] - pub fn width(&self) -> f32 { + fn width(&self) -> f32 { self.size.width } - /// Returns the height of the [`Frame`]. #[inline] - pub fn height(&self) -> f32 { + fn height(&self) -> f32 { self.size.height } - /// Returns the dimensions of the [`Frame`]. #[inline] - pub fn size(&self) -> Size { + fn size(&self) -> Size { self.size } - /// Returns the coordinate of the center of the [`Frame`]. #[inline] - pub fn center(&self) -> Point { + fn center(&self) -> Point { Point::new(self.size.width / 2.0, self.size.height / 2.0) } - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { let Fill { style, rule } = fill.into(); let mut buffer = self @@ -239,9 +136,7 @@ impl Frame { .expect("Tessellate path."); } - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( + fn fill_rectangle( &mut self, top_left: Point, size: Size, @@ -276,9 +171,7 @@ impl Frame { .expect("Fill rectangle"); } - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { let stroke = stroke.into(); let mut buffer = self @@ -315,20 +208,7 @@ impl Frame { .expect("Stroke path"); } - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a `Canvas`. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - pub fn fill_text(&mut self, text: impl Into<Text>) { + fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); let (scale_x, scale_y) = self.transforms.current.scale(); @@ -384,57 +264,55 @@ impl Frame { } } - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. #[inline] - pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R { - self.push_transform(); - - let result = f(self); - - self.pop_transform(); - - result + fn translate(&mut self, translation: Vector) { + self.transforms.current.0 = + self.transforms + .current + .0 + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); } - /// Pushes the current transform in the transform stack. - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); + #[inline] + fn rotate(&mut self, angle: impl Into<Radians>) { + self.transforms.current.0 = self + .transforms + .current + .0 + .pre_rotate(lyon::math::Angle::radians(angle.into().0)); } - /// Pops a transform from the transform stack and sets it as the current transform. - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); + #[inline] + fn scale(&mut self, scale: impl Into<f32>) { + let scale = scale.into(); + + self.scale_nonuniform(Vector { x: scale, y: scale }); } - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. #[inline] - pub fn with_clip<R>( - &mut self, - region: Rectangle, - f: impl FnOnce(&mut Frame) -> R, - ) -> R { - let mut frame = Frame::new(region.size()); + fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + let scale = scale.into(); - let result = f(&mut frame); + self.transforms.current.0 = + self.transforms.current.0.pre_scale(scale.x, scale.y); + } - let origin = Point::new(region.x, region.y); + fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } - self.clip(frame, origin); + fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } - result + fn draft(&mut self, size: Size) -> Frame { + Frame::new(size) } - /// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`]. - pub fn clip(&mut self, frame: Frame, at: Point) { + fn paste(&mut self, frame: Frame, at: Point) { let size = frame.size(); let primitives = frame.into_primitives(); let transformation = Transformation::translate(at.x, at.y); @@ -462,89 +340,151 @@ impl Frame { }); } - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.0 = - self.transforms - .current - .0 - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); + fn into_geometry(self) -> Self::Geometry { + Primitive::Group { + primitives: self.into_primitives(), + } } +} - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: impl Into<Radians>) { - self.transforms.current.0 = self - .transforms - .current - .0 - .pre_rotate(lyon::math::Angle::radians(angle.into().0)); +enum Buffer { + Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>), + Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>), +} + +struct BufferStack { + stack: Vec<Buffer>, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } } - /// Applies a uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: impl Into<f32>) { - let scale = scale.into(); + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(_) => match self.stack.last() { + Some(Buffer::Gradient(_)) => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + )); + } + }, + } - self.scale_nonuniform(Vector { x: scale, y: scale }); + self.stack.last_mut().unwrap() } - /// Applies a non-uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { - let scale = scale.into(); + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box<dyn tessellation::FillGeometryBuilder + 'a> { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color::pack(*color)), + )) + } + (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + GradientVertex2DBuilder { + gradient: gradient.pack(), + }, + )) + } + _ => unreachable!(), + } + } - self.transforms.current.0 = - self.transforms.current.0.pre_scale(scale.x, scale.y); + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color::pack(*color)), + )) + } + (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + GradientVertex2DBuilder { + gradient: gradient.pack(), + }, + )) + } + _ => unreachable!(), + } } +} - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), +#[derive(Debug)] +struct Transforms { + previous: Vec<Transform>, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform(lyon::math::Transform); + +impl Transform { + fn is_identity(&self) -> bool { + self.0 == lyon::math::Transform::identity() + } + + fn is_scale_translation(&self) -> bool { + self.0.m12.abs() < 2.0 * f32::EPSILON + && self.0.m21.abs() < 2.0 * f32::EPSILON + } + + fn scale(&self) -> (f32, f32) { + (self.0.m11, self.0.m22) + } + + fn transform_point(&self, point: Point) -> Point { + let transformed = self + .0 + .transform_point(euclid::Point2D::new(point.x, point.y)); + + Point { + x: transformed.x, + y: transformed.y, } } - fn into_primitives(mut self) -> Vec<Primitive> { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - Buffer::Gradient(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) } } + } - self.primitives + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + match &mut gradient { + Gradient::Linear(linear) => { + linear.start = self.transform_point(linear.start); + linear.end = self.transform_point(linear.end); + } + } + + gradient } } - struct GradientVertex2DBuilder { gradient: gradient::Packed, } diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index fff927ea..ee9af93c 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -28,3 +28,11 @@ impl Damage for Custom { } } } + +impl TryFrom<Mesh> for Custom { + type Error = &'static str; + + fn try_from(mesh: Mesh) -> Result<Self, Self::Error> { + Ok(Custom::Mesh(mesh)) + } +} diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs index c6b7c5e2..814440ba 100644 --- a/wgpu/src/primitive/pipeline.rs +++ b/wgpu/src/primitive/pipeline.rs @@ -1,5 +1,5 @@ //! Draw primitives using custom pipelines. -use crate::core::{Rectangle, Size}; +use crate::core::{self, Rectangle, Size}; use std::any::{Any, TypeId}; use std::collections::HashMap; @@ -58,7 +58,7 @@ pub trait Primitive: Debug + Send + Sync + 'static { } /// A renderer than can draw custom pipeline primitives. -pub trait Renderer: crate::core::Renderer { +pub trait Renderer: core::Renderer { /// Draws a custom pipeline primitive. fn draw_pipeline_primitive( &mut self, diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index c9338fec..828d9e09 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,6 +1,6 @@ //! Configure a renderer. use crate::core::{Font, Pixels}; -use crate::graphics::Antialiasing; +use crate::graphics::{self, Antialiasing}; /// The settings of a [`Backend`]. /// @@ -29,30 +29,6 @@ pub struct Settings { pub antialiasing: Option<Antialiasing>, } -impl Settings { - /// Creates new [`Settings`] using environment configuration. - /// - /// Specifically: - /// - /// - The `internal_backend` can be configured using the `WGPU_BACKEND` - /// environment variable. If the variable is not set, the primary backend - /// will be used. The following values are allowed: - /// - `vulkan` - /// - `metal` - /// - `dx12` - /// - `dx11` - /// - `gl` - /// - `webgpu` - /// - `primary` - pub fn from_env() -> Self { - Settings { - internal_backend: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::all()), - ..Self::default() - } - } -} - impl Default for Settings { fn default() -> Settings { Settings { @@ -64,3 +40,14 @@ impl Default for Settings { } } } + +impl From<graphics::Settings> for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + ..Settings::default() + } + } +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index fa6b9373..9a3e3b34 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,13 +1,11 @@ //! Connect a window with a renderer. use crate::core::{Color, Size}; -use crate::graphics; use crate::graphics::color; use crate::graphics::compositor; -use crate::graphics::{Error, Viewport}; +use crate::graphics::error; +use crate::graphics::{self, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use std::future::Future; - /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { @@ -20,6 +18,32 @@ pub struct Compositor { alpha_mode: wgpu::CompositeAlphaMode, } +/// A compositor error. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// The surface creation failed. + #[error("the surface creation failed: {0}")] + SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError), + /// The surface is not compatible. + #[error("the surface is not compatible")] + IncompatibleSurface, + /// No adapter was found for the options requested. + #[error("no adapter was found for the options requested: {0:?}")] + NoAdapterFound(String), + /// No device request succeeded. + #[error("no device request succeeded: {0:?}")] + RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>), +} + +impl From<Error> for graphics::Error { + fn from(error: Error) -> Self { + Self::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::RequestFailed(error.to_string()), + } + } +} + impl Compositor { /// Requests a new [`Compositor`] with the given [`Settings`]. /// @@ -27,7 +51,7 @@ impl Compositor { pub async fn request<W: compositor::Window>( settings: Settings, compatible_window: Option<W>, - ) -> Option<Self> { + ) -> Result<Self, Error> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.internal_backend, ..Default::default() @@ -49,23 +73,27 @@ impl Compositor { let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); + let adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }; + let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower - } else { - wgpu::PowerPreference::HighPerformance - }), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }) - .await?; + .request_adapter(&adapter_options) + .await + .ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?; log::info!("Selected: {:#?}", adapter.get_info()); - let (format, alpha_mode) = - compatible_surface.as_ref().and_then(|surface| { + let (format, alpha_mode) = compatible_surface + .as_ref() + .and_then(|surface| { let capabilities = surface.get_capabilities(&adapter); let mut formats = capabilities.formats.iter().copied(); @@ -101,7 +129,8 @@ impl Compositor { }; format.zip(Some(preferred_alpha)) - })?; + }) + .ok_or(Error::IncompatibleSurface)?; log::info!( "Selected format: {format:?} with alpha mode: {alpha_mode:?}" @@ -115,39 +144,46 @@ impl Compositor { let limits = [wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()]; - let mut limits = limits.into_iter().map(|limits| wgpu::Limits { + let limits = limits.into_iter().map(|limits| wgpu::Limits { max_bind_groups: 2, ..limits }); - let (device, queue) = - loop { - let required_limits = limits.next()?; - let device = adapter.request_device( + let mut errors = Vec::new(); + + for required_limits in limits { + let result = adapter + .request_device( &wgpu::DeviceDescriptor { label: Some( "iced_wgpu::window::compositor device descriptor", ), required_features: wgpu::Features::empty(), - required_limits, + required_limits: required_limits.clone(), }, None, - ).await.ok(); - - if let Some(device) = device { - break Some(device); + ) + .await; + + match result { + Ok((device, queue)) => { + return Ok(Compositor { + instance, + settings, + adapter, + device, + queue, + format, + alpha_mode, + }) } - }?; - - Some(Compositor { - instance, - settings, - adapter, - device, - queue, - format, - alpha_mode, - }) + Err(error) => { + errors.push((required_limits, error)); + } + } + } + + Err(Error::RequestDeviceFailed(errors)) } /// Creates a new rendering [`Backend`] for this [`Compositor`]. @@ -168,9 +204,7 @@ pub async fn new<W: compositor::Window>( settings: Settings, compatible_window: W, ) -> Result<Compositor, Error> { - Compositor::request(settings, Some(compatible_window)) - .await - .ok_or(Error::GraphicsAdapterNotFound) + Compositor::request(settings, Some(compatible_window)).await } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. @@ -229,15 +263,31 @@ pub fn present<T: AsRef<str>>( } impl graphics::Compositor for Compositor { - type Settings = Settings; type Renderer = Renderer; type Surface = wgpu::Surface<'static>; - fn new<W: compositor::Window>( - settings: Self::Settings, + async fn with_backend<W: compositor::Window>( + settings: graphics::Settings, compatible_window: W, - ) -> impl Future<Output = Result<Self, Error>> { - new(settings, compatible_window) + backend: Option<&str>, + ) -> Result<Self, graphics::Error> { + match backend { + None | Some("wgpu") => Ok(new( + Settings { + internal_backend: wgpu::util::backend_bits_from_env() + .unwrap_or(wgpu::Backends::all()), + ..settings.into() + }, + compatible_window, + ) + .await?), + Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::DidNotMatch { + preferred_backend: backend.to_owned(), + }, + }), + } } fn create_renderer(&self) -> Self::Renderer { diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3c9ffddb..a45f47ef 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -21,6 +21,7 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] +advanced = [] [dependencies] iced_renderer.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..7a21895a 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,8 +6,10 @@ mod program; pub use event::Event; pub use program::Program; -pub use crate::graphics::geometry::*; -pub use crate::renderer::geometry::*; +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}; @@ -21,6 +23,19 @@ 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 +57,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()); /// @@ -210,9 +225,12 @@ where renderer.with_transformation( Transformation::translate(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..48f6abf6 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 { @@ -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/combo_box.rs b/widget/src/combo_box.rs index ee24d742..e4f4a41f 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> { @@ -686,7 +718,7 @@ where (self.on_selected)(x) }, self.on_option_hovered.as_deref(), - &self.menu_style, + &self.menu_class, ) .width(bounds.width) .padding(self.padding); @@ -712,7 +744,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 +752,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 +809,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..21405722 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, } } @@ -136,27 +128,37 @@ where self } - /// 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 - } - /// Sets whether the contents of the [`Container`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { 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 +274,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 +319,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 +357,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 +497,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 +508,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 +525,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 +534,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 4863e550..77b30882 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,6 +7,7 @@ use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; use crate::keyed; +use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; @@ -58,7 +59,7 @@ 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) @@ -104,7 +105,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) @@ -117,7 +118,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) @@ -134,7 +135,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) @@ -147,7 +148,7 @@ pub fn text<'a, Theme, Renderer>( text: impl ToString, ) -> Text<'a, Theme, Renderer> where - Theme: text::DefaultStyle + 'a, + Theme: text::Catalog + 'a, Renderer: core::text::Renderer, { Text::new(text.to_string()) @@ -161,7 +162,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) @@ -178,7 +179,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, { @@ -194,7 +195,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) @@ -209,7 +210,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) @@ -223,7 +224,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) @@ -240,7 +241,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) } @@ -256,7 +257,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) } @@ -274,7 +275,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) @@ -291,7 +292,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) @@ -318,7 +319,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) } @@ -328,7 +329,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) } @@ -345,7 +346,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) } @@ -367,7 +368,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) } @@ -395,7 +396,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..f673c7b3 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -93,7 +93,7 @@ where { // The raw w/h of the underlying image let image_size = { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.measure_image(handle); Size::new(width as f32, height as f32) }; @@ -130,7 +130,7 @@ pub fn draw<Renderer, Handle>( Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, { - 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 bounds = layout.bounds(); @@ -148,7 +148,11 @@ pub fn draw<Renderer, Handle>( ..bounds }; - renderer.draw(handle.clone(), filter_method, drawing_bounds + offset); + renderer.draw_image( + handle.clone(), + filter_method, + drawing_bounds + offset, + ); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index bd10e953..fba00028 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -127,7 +127,7 @@ where limits: &layout::Limits, ) -> layout::Node { let image_size = { - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_image(&self.handle); Size::new(width as f32, height as f32) }; let raw_size = limits.resolve(self.width, self.height, image_size); @@ -344,8 +344,7 @@ where height: image_size.height, ..bounds }; - - renderer.draw( + renderer.draw_image( self.handle.clone(), self.filter_method, drawing_bounds, @@ -429,7 +428,7 @@ pub fn image_size<Renderer>( where Renderer: image::Renderer, { - let size = renderer.dimensions(handle); + let size = renderer.measure_image(handle); let size = Size::new(size.width as f32, size.height as f32); let size = content_fit.fit(size, bounds); diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 0364f980..d76caa8a 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,14 +513,14 @@ 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, ); } @@ -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/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..801e792b 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 { @@ -478,7 +495,7 @@ where bounds.x + bounds.width - self.padding.right, bounds.center_y(), ), - appearance.handle_color, + style.handle_color, *viewport, ); } @@ -505,9 +522,9 @@ where }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { - appearance.text_color + style.text_color } else { - appearance.placeholder_color + style.placeholder_color }, *viewport, ); @@ -539,7 +556,7 @@ where (on_select)(option) }, None, - &self.style.menu, + &self.menu_class, ) .width(bounds.width) .padding(self.padding) @@ -565,7 +582,7 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone + 'a, - Theme: 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -662,7 +679,7 @@ pub enum Status { /// The appearance of a pick list. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The text [`Color`] of the pick list. pub text_color: Color, /// The placeholder [`Color`] of the pick list. @@ -675,36 +692,49 @@ pub struct Appearance { pub border: Border, } -/// The styles of the different parts of a [`PickList`]. -#[allow(missing_debug_implementations)] -pub struct Style<'a, Theme> { - /// The style of the [`PickList`] itself. - pub field: Box<dyn Fn(&Theme, Status) -> Appearance + 'a>, +/// The theme catalog of a [`PickList`]. +pub trait Catalog: menu::Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; - /// The style of the [`Menu`] of the pick list. - pub menu: menu::Style<'a, Theme>, -} + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; -/// The default style of a [`PickList`]. -pub trait DefaultStyle: Sized { - /// Returns the default style of a [`PickList`]. - fn default_style() -> Style<'static, Self>; + /// The default class for the menu of the [`PickList`]. + fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> { + <Self as menu::Catalog>::default() + } + + /// The [`Style`] of a class with the given status. + fn style( + &self, + class: &<Self as Catalog>::Class<'_>, + status: Status, + ) -> Style; } -impl DefaultStyle for Theme { - fn default_style() -> Style<'static, Self> { - Style { - field: Box::new(default), - menu: menu::DefaultStyle::default_style(), - } +/// A styling function for a [`PickList`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) + } + + fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style { + class(self, status) } } /// The default style of the field of a [`PickList`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let active = Appearance { + let active = Style { text_color: palette.background.weak.text, background: palette.background.weak.color.into(), placeholder_color: palette.background.strong.color, @@ -718,7 +748,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { match status { Status::Active => active, - Status::Hovered | Status::Opened => Appearance { + Status::Hovered | Status::Opened => Style { border: Border { color: palette.primary.strong.color, ..active.border diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 38d8da85..e7821b43 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -4,7 +4,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - Background, Border, Element, Layout, Length, Rectangle, Size, Theme, Widget, + self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme, + Widget, }; use std::ops::RangeInclusive; @@ -22,15 +23,21 @@ use std::ops::RangeInclusive; /// ///  #[allow(missing_debug_implementations)] -pub struct ProgressBar<'a, Theme = crate::Theme> { +pub struct ProgressBar<'a, Theme = crate::Theme> +where + Theme: Catalog, +{ range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } -impl<'a, Theme> ProgressBar<'a, Theme> { +impl<'a, Theme> ProgressBar<'a, Theme> +where + Theme: Catalog, +{ /// The default height of a [`ProgressBar`]. pub const DEFAULT_HEIGHT: f32 = 30.0; @@ -39,16 +46,13 @@ impl<'a, Theme> ProgressBar<'a, Theme> { /// It expects: /// * an inclusive range of possible values /// * the current value of the [`ProgressBar`] - pub fn new(range: RangeInclusive<f32>, value: f32) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { ProgressBar { value: value.clamp(*range.start(), *range.end()), range, width: Length::Fill, height: None, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -65,8 +69,20 @@ impl<'a, Theme> ProgressBar<'a, Theme> { } /// Sets the style of the [`ProgressBar`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`ProgressBar`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -74,7 +90,8 @@ impl<'a, Theme> ProgressBar<'a, Theme> { impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for ProgressBar<'a, Theme> where - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn size(&self) -> Size<Length> { Size { @@ -116,15 +133,15 @@ where / (range_end - range_start) }; - let appearance = (self.style)(theme); + let style = theme.style(&self.class); renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); if active_progress_width > 0.0 { @@ -134,10 +151,10 @@ where width: active_progress_width, ..bounds }, - border: Border::rounded(appearance.border.radius), + border: Border::rounded(style.border.radius), ..renderer::Quad::default() }, - appearance.bar, + style.bar, ); } } @@ -147,8 +164,8 @@ impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + core::Renderer, { fn from( progress_bar: ProgressBar<'a, Theme>, @@ -159,7 +176,7 @@ where /// The appearance of a progress bar. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the progress bar. pub background: Background, /// The [`Background`] of the bar of the progress bar. @@ -168,29 +185,37 @@ pub struct Appearance { pub border: Border, } -/// The style of a [`ProgressBar`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`ProgressBar`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; -/// The default style of a [`ProgressBar`]. -pub trait DefaultStyle { - /// Returns the default style of a [`ProgressBar`]. - fn default_style(&self) -> Appearance; + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - primary(self) +/// A styling function for a [`ProgressBar`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) } -} -impl DefaultStyle for Appearance { - fn default_style(&self) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } /// The primary style of a [`ProgressBar`]. -pub fn primary(theme: &Theme) -> Appearance { +pub fn primary(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled( @@ -200,7 +225,7 @@ pub fn primary(theme: &Theme) -> Appearance { } /// The secondary style of a [`ProgressBar`]. -pub fn secondary(theme: &Theme) -> Appearance { +pub fn secondary(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled( @@ -210,14 +235,14 @@ pub fn secondary(theme: &Theme) -> Appearance { } /// The success style of a [`ProgressBar`]. -pub fn success(theme: &Theme) -> Appearance { +pub fn success(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled(palette.background.strong.color, palette.success.base.color) } /// The danger style of a [`ProgressBar`]. -pub fn danger(theme: &Theme) -> Appearance { +pub fn danger(theme: &Theme) -> Style { let palette = theme.extended_palette(); styled(palette.background.strong.color, palette.danger.base.color) @@ -226,8 +251,8 @@ pub fn danger(theme: &Theme) -> Appearance { fn styled( background: impl Into<Background>, bar: impl Into<Background>, -) -> Appearance { - Appearance { +) -> Style { + Style { background: background.into(), bar: bar.into(), border: Border::rounded(2), diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 90c0c970..e064aada 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -8,7 +8,6 @@ use crate::core::{ Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector, Widget, }; -use crate::graphics::geometry::Renderer as _; use crate::Renderer; use std::cell::RefCell; @@ -20,22 +19,25 @@ const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which /// can be read by an imaging device, such as a camera. #[allow(missing_debug_implementations)] -pub struct QRCode<'a, Theme = crate::Theme> { +pub struct QRCode<'a, Theme = crate::Theme> +where + Theme: Catalog, +{ data: &'a Data, cell_size: u16, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } -impl<'a, Theme> QRCode<'a, Theme> { +impl<'a, Theme> QRCode<'a, Theme> +where + Theme: Catalog, +{ /// Creates a new [`QRCode`] with the provided [`Data`]. - pub fn new(data: &'a Data) -> Self - where - Theme: DefaultStyle + 'a, - { + pub fn new(data: &'a Data) -> Self { Self { data, cell_size: DEFAULT_CELL_SIZE, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -46,14 +48,27 @@ impl<'a, Theme> QRCode<'a, Theme> { } /// Sets the style of the [`QRCode`]. - pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`QRCode`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } -impl<'a, Message, Theme> Widget<Message, Theme, Renderer> - for QRCode<'a, Theme> +impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> +where + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -97,13 +112,13 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> let bounds = layout.bounds(); let side_length = self.data.width + 2 * QUIET_ZONE; - let appearance = (self.style)(theme); - let mut last_appearance = state.last_appearance.borrow_mut(); + let style = theme.style(&self.class); + let mut last_style = state.last_style.borrow_mut(); - if Some(appearance) != *last_appearance { + if Some(style) != *last_style { self.data.cache.clear(); - *last_appearance = Some(appearance); + *last_style = Some(style); } // Reuse cache if possible @@ -115,7 +130,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> frame.fill_rectangle( Point::ORIGIN, Size::new(side_length as f32, side_length as f32), - appearance.background, + style.background, ); // Avoid drawing on the quiet zone @@ -134,7 +149,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> frame.fill_rectangle( Point::new(column as f32, row as f32), Size::UNIT, - appearance.cell, + style.cell, ); }); }); @@ -142,7 +157,9 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> renderer.with_translation( bounds.position() - Point::ORIGIN, |renderer| { - renderer.draw(vec![geometry]); + use crate::graphics::geometry::Renderer as _; + + renderer.draw_geometry(geometry); }, ); } @@ -151,7 +168,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> impl<'a, Message, Theme> From<QRCode<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: 'a, + Theme: Catalog + 'a, { fn from(qr_code: QRCode<'a, Theme>) -> Self { Self::new(qr_code) @@ -165,7 +182,7 @@ where pub struct Data { contents: Vec<qrcode::Color>, width: usize, - cache: canvas::Cache, + cache: canvas::Cache<Renderer>, } impl Data { @@ -323,44 +340,50 @@ impl From<qrcode::types::QrError> for Error { #[derive(Default)] struct State { - last_appearance: RefCell<Option<Appearance>>, + last_style: RefCell<Option<Style>>, } /// The appearance of a QR code. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { +pub struct Style { /// The color of the QR code data cells pub cell: Color, /// The color of the QR code background pub background: Color, } -/// The style of a [`QRCode`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; +/// The theme catalog of a [`QRCode`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`QRCode`]. -pub trait DefaultStyle { - /// Returns the default style of a [`QRCode`]. - fn default_style(&self) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) +/// A styling function for a [`QRCode`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) } } /// The default style of a [`QRCode`]. -pub fn default(theme: &Theme) -> Appearance { +pub fn default(theme: &Theme) -> Style { let palette = theme.palette(); - Appearance { + Style { cell: palette.text, background: palette.background, } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index a7b7dd03..6b22961d 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -69,6 +69,7 @@ use crate::core::{ #[allow(missing_debug_implementations)] pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where + Theme: Catalog, Renderer: text::Renderer, { is_selected: bool, @@ -81,12 +82,13 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> where Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { /// The default size of a [`Radio`] button. @@ -110,7 +112,6 @@ where f: F, ) -> Self where - Theme: DefaultStyle + 'a, V: Eq + Copy, F: FnOnce(V) -> Message, { @@ -125,7 +126,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -175,11 +176,20 @@ where } /// Sets the style of the [`Radio`] button. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Radio`] button. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -188,6 +198,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Radio<'a, Message, Theme, Renderer> where Message: Clone, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -284,7 +295,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -300,7 +311,7 @@ where Status::Active { is_selected } }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); { let layout = children.next().unwrap(); @@ -314,12 +325,12 @@ where bounds, border: Border { radius: (size / 2.0).into(), - width: appearance.border_width, - color: appearance.border_color, + width: style.border_width, + color: style.border_color, }, ..renderer::Quad::default() }, - appearance.background, + style.background, ); if self.is_selected { @@ -334,7 +345,7 @@ where border: Border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, - appearance.dot_color, + style.dot_color, ); } } @@ -344,11 +355,11 @@ where crate::text::draw( renderer, - style, + defaults, label_layout, tree.state.downcast_ref(), - crate::text::Appearance { - color: appearance.text_color, + crate::text::Style { + color: style.text_color, }, viewport, ); @@ -360,7 +371,7 @@ impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: 'a, + Theme: 'a + Catalog, Renderer: 'a + text::Renderer, { fn from( @@ -387,7 +398,7 @@ pub enum Status { /// The appearance of a radio button. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The [`Background`] of the radio button. pub background: Background, /// The [`Color`] of the dot of the radio button. @@ -400,32 +411,38 @@ pub struct Appearance { pub text_color: Option<Color>, } -/// The style of a [`Radio`] button. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Radio`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Radio`] button. -pub trait DefaultStyle { - /// Returns the default style of a [`Radio`] button. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`Radio`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`Radio`] button. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let active = Appearance { + let active = Style { background: Color::TRANSPARENT.into(), dot_color: palette.primary.strong.color, border_width: 1.0, @@ -435,7 +452,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { match status { Status::Active { .. } => active, - Status::Hovered { .. } => Appearance { + Status::Hovered { .. } => Style { dot_color: palette.primary.strong.color, background: palette.primary.weak.color.into(), ..active diff --git a/widget/src/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..84e9ac15 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>() @@ -651,7 +648,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -701,13 +698,9 @@ 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() { @@ -719,7 +712,7 @@ where &tree.children[0], renderer, theme, - style, + defaults, content_layout, cursor, &Rectangle { @@ -782,7 +775,7 @@ where if let Some(scrollbar) = scrollbars.y { draw_scrollbar( renderer, - appearance.vertical_scrollbar, + style.vertical_scrollbar, &scrollbar, ); } @@ -790,14 +783,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 +814,7 @@ where &tree.children[0], renderer, theme, - style, + defaults, content_layout, cursor, &Rectangle { @@ -916,8 +909,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>, @@ -1570,9 +1563,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 +1594,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 +1635,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 +1653,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 +1680,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/slider.rs b/widget/src/slider.rs index d3b46a98..a8f1d192 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -9,7 +9,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Widget, }; @@ -39,7 +39,10 @@ use std::ops::RangeInclusive; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Theme = crate::Theme> { +pub struct Slider<'a, T, Message, Theme = crate::Theme> +where + Theme: Catalog, +{ range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -49,13 +52,14 @@ pub struct Slider<'a, T, Message, Theme = crate::Theme> { on_release: Option<Message>, width: Length, height: f32, - style: Style<'a, Theme>, + class: Theme::Class<'a>, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where T: Copy + From<u8> + PartialOrd, Message: Clone, + Theme: Catalog, { /// The default height of a [`Slider`]. pub const DEFAULT_HEIGHT: f32 = 16.0; @@ -70,7 +74,6 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where - Theme: DefaultStyle + 'a, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -95,7 +98,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style: Box::new(Theme::default_style), + class: Theme::default(), } } @@ -130,15 +133,6 @@ where self } - /// Sets the style of the [`Slider`]. - pub fn style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); - self - } - /// Sets the step size of the [`Slider`]. pub fn step(mut self, step: impl Into<T>) -> Self { self.step = step.into(); @@ -152,6 +146,24 @@ where self.shift_step = Some(shift_step.into()); self } + + /// Sets the style of the [`Slider`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Slider`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -159,7 +171,8 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -349,8 +362,8 @@ where let bounds = layout.bounds(); let is_mouse_over = cursor.is_over(bounds); - let style = (self.style)( - theme, + let style = theme.style( + &self.class, if state.is_dragging { Status::Dragged } else if is_mouse_over { @@ -461,8 +474,8 @@ impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>> where T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, Message: Clone + 'a, - Theme: 'a, - Renderer: crate::core::Renderer + 'a, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( slider: Slider<'a, T, Message, Theme>, @@ -490,15 +503,15 @@ pub enum Status { /// The appearance of a slider. #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The colors of the rail of the slider. pub rail: Rail, /// The appearance of the [`Handle`] of the slider. pub handle: Handle, } -impl Appearance { - /// Changes the [`HandleShape`] of the [`Appearance`] to a circle +impl Style { + /// Changes the [`HandleShape`] of the [`Style`] to a circle /// with the given radius. pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self { self.handle.shape = HandleShape::Circle { @@ -549,29 +562,35 @@ pub enum HandleShape { }, } -/// The style of a [`Slider`]. -pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; +/// The theme catalog of a [`Slider`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; -/// The default style of a [`Slider`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Slider`]. - fn default_style(&self, status: Status) -> Appearance; + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; } -impl DefaultStyle for Theme { - fn default_style(&self, status: Status) -> Appearance { - default(self, status) +/// A styling function for a [`Slider`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) } -} -impl DefaultStyle for Appearance { - fn default_style(&self, _status: Status) -> Appearance { - *self + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) } } /// The default style of a [`Slider`]. -pub fn default(theme: &Theme, status: Status) -> Appearance { +pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let color = match status { @@ -580,7 +599,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance { Status::Dragged => palette.primary.strong.color, }; - Appearance { + Style { rail: Rail { colors: (color, palette.secondary.base.color), width: 4.0, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 1ac07ade..eb142189 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -20,36 +20,36 @@ 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>, } -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(), } } /// 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 +78,21 @@ 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) -> 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 style( - mut self, - style: impl Fn(&Theme, Status) -> Appearance + 'a, - ) -> Self { - self.style = Box::new(style); + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -93,6 +101,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,7 +117,7 @@ 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 size to be available to the widget prior to `Shrink`ing @@ -142,7 +151,7 @@ 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 bounds = layout.bounds(); @@ -167,11 +176,11 @@ where Status::Idle }; - let appearance = (self.style)(theme, status); + let style = theme.style(&self.class, status); - renderer.draw( + renderer.draw_svg( self.handle.clone(), - appearance.color, + style.color, drawing_bounds + offset, ); }; @@ -189,7 +198,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 +217,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 +226,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 class produced by the [`Catalog`]. + fn default<'a>() -> Self::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 [`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..a00df3c7 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`]. @@ -134,7 +134,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 +142,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 } } @@ -309,6 +318,7 @@ 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 +489,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 +518,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, ); @@ -555,7 +565,7 @@ where }, ..renderer::Quad::default() }, - appearance.value, + style.value, ); } } @@ -568,7 +578,7 @@ where bounds: range, ..renderer::Quad::default() }, - appearance.selection, + style.selection, ); } } @@ -604,7 +614,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 +806,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 +821,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 +867,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..dafe2fca 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 } @@ -345,15 +342,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,7 +359,7 @@ where renderer.fill_paragraph( &state.icon, icon_layout.bounds().center(), - appearance.icon, + style.icon, *viewport, ); } @@ -401,7 +398,7 @@ where }, ..renderer::Quad::default() }, - appearance.value, + style.value, )) } else { None @@ -440,7 +437,7 @@ where }, ..renderer::Quad::default() }, - appearance.selection, + style.selection, )), if end == right { right_offset @@ -475,9 +472,9 @@ 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, ); @@ -496,6 +493,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 { @@ -1058,8 +1056,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 +1398,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 +1413,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 +1461,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( diff --git a/winit/src/application.rs b/winit/src/application.rs index 13d9282d..d68523fa 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -13,6 +13,7 @@ use crate::core::window; use crate::core::{Color, Event, Point, Size, Theme}; use crate::futures::futures; use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics; use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; @@ -130,7 +131,7 @@ pub fn default(theme: &Theme) -> Appearance { /// settings. pub async fn run<A, E, C>( settings: Settings<A::Flags>, - compositor_settings: C::Settings, + graphics_settings: graphics::Settings, ) -> Result<(), Error> where A: Application + 'static, @@ -219,7 +220,7 @@ where }; } - let compositor = C::new(compositor_settings, window.clone()).await?; + let compositor = C::new(graphics_settings, window.clone()).await?; let mut renderer = compositor.create_renderer(); for font in settings.fonts { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 18db1fb5..b4c25411 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -16,6 +16,7 @@ use crate::futures::futures::executor; use crate::futures::futures::task; use crate::futures::futures::{Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics; use crate::graphics::{compositor, Compositor}; use crate::multi_window::window_manager::WindowManager; use crate::runtime::command::{self, Command}; @@ -105,7 +106,7 @@ where /// settings. pub fn run<A, E, C>( settings: Settings<A::Flags>, - compositor_settings: C::Settings, + graphics_settings: graphics::Settings, ) -> Result<(), Error> where A: Application + 'static, @@ -187,7 +188,7 @@ where } let mut compositor = - executor::block_on(C::new(compositor_settings, main_window.clone()))?; + executor::block_on(C::new(graphics_settings, main_window.clone()))?; let mut window_manager = WindowManager::new(); let _ = window_manager.insert( |