From cbe4603579c6af55c35254257981e14cd4a67897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 3 Mar 2024 01:02:23 +0100 Subject: [PATCH 001/281] Remove complex cross-axis layout logic from `Column` and `Row` --- core/src/layout/flex.rs | 67 ++++++++++++------------------------- examples/layout/src/main.rs | 42 ++--------------------- 2 files changed, 25 insertions(+), 84 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 40bd7123..dcb4d8de 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -80,14 +80,9 @@ where let mut fill_main_sum = 0; let mut cross = match axis { - Axis::Horizontal => match height { - Length::Shrink => 0.0, - _ => max_cross, - }, - Axis::Vertical => match width { - Length::Shrink => 0.0, - _ => max_cross, - }, + Axis::Vertical if width == Length::Shrink => 0.0, + Axis::Horizontal if height == Length::Shrink => 0.0, + _ => max_cross, }; let mut available = axis.main(limits.max()) - total_spacing; @@ -103,35 +98,14 @@ where }; if fill_main_factor == 0 { - if fill_cross_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = - child.as_widget().layout(tree, renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - cross = cross.max(axis.cross(size)); - - nodes[i] = layout; - } - } else { - fill_main_sum += fill_main_factor; - } - } - - for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let (fill_main_factor, fill_cross_factor) = { - let size = child.as_widget().size(); - - axis.pack(size.width.fill_factor(), size.height.fill_factor()) - }; - - if fill_main_factor == 0 && fill_cross_factor != 0 { - let (max_width, max_height) = axis.pack(available, cross); + let (max_width, max_height) = axis.pack( + available, + if fill_cross_factor == 0 { + max_cross + } else { + cross + }, + ); let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); @@ -141,9 +115,11 @@ where let size = layout.size(); available -= axis.main(size); - cross = cross.max(axis.cross(layout.size())); + cross = cross.max(axis.cross(size)); nodes[i] = layout; + } else { + fill_main_sum += fill_main_factor; } } @@ -175,14 +151,15 @@ where max_main }; - let max_cross = if fill_cross_factor == 0 { - max_cross - } else { - cross - }; - let (min_width, min_height) = axis.pack(min_main, 0.0); - let (max_width, max_height) = axis.pack(max_main, max_cross); + let (max_width, max_height) = axis.pack( + max_main, + if fill_cross_factor == 0 { + max_cross + } else { + cross + }, + ); let child_limits = Limits::new( Size::new(min_width, min_height), diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 06a476be..39c8315f 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -4,11 +4,11 @@ use iced::mouse; use iced::theme; use iced::widget::{ button, canvas, checkbox, column, container, horizontal_space, pick_list, - row, scrollable, text, vertical_rule, + row, scrollable, text, }; use iced::{ - color, Alignment, Application, Color, Command, Element, Font, Length, - Point, Rectangle, Renderer, Settings, Subscription, Theme, + color, Alignment, Application, Command, Element, Font, Length, Point, + Rectangle, Renderer, Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -167,10 +167,6 @@ impl Example { title: "Application", view: application, }, - Self { - title: "Nested Quotes", - view: nested_quotes, - }, ]; fn is_first(self) -> bool { @@ -304,38 +300,6 @@ fn application<'a>() -> Element<'a, Message> { column![header, row![sidebar, content]].into() } -fn nested_quotes<'a>() -> Element<'a, Message> { - (1..5) - .fold(column![text("Original text")].padding(10), |quotes, i| { - column![ - container( - row![vertical_rule(2), quotes].height(Length::Shrink) - ) - .style(|theme: &Theme| { - let palette = theme.extended_palette(); - - container::Appearance::default().with_background( - if palette.is_dark { - Color { - a: 0.01, - ..Color::WHITE - } - } else { - Color { - a: 0.08, - ..Color::BLACK - } - }, - ) - }), - text(format!("Reply {i}")) - ] - .spacing(10) - .padding(10) - }) - .into() -} - fn square<'a>(size: impl Into + Copy) -> Element<'a, Message> { struct Square; From ce309db37b8eb9860ae1f1be1710fb39e7a9edea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Mar 2024 03:57:03 +0100 Subject: [PATCH 002/281] Try new approach to theming for `Slider` --- style/src/lib.rs | 2 +- style/src/slider.rs | 31 +- style/src/theme.rs | 108 ++----- widget/src/slider.rs | 552 +++++++++++++++------------------ widget/src/vertical_slider.rs | 562 +++++++++++++++------------------- 5 files changed, 537 insertions(+), 718 deletions(-) diff --git a/style/src/lib.rs b/style/src/lib.rs index 3c2865eb..17ba09c4 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( unused_results, - missing_docs, + // missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/style/src/slider.rs b/style/src/slider.rs index bf1c7329..0c19e47d 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -1,6 +1,6 @@ //! Change the apperance of a slider. use crate::core::border; -use crate::core::Color; +use crate::core::{Color, Pixels}; /// The appearance of a slider. #[derive(Debug, Clone, Copy)] @@ -11,6 +11,17 @@ pub struct Appearance { pub handle: Handle, } +impl Appearance { + /// Changes the [`HandleShape`] of the [`Appearance`] to a circle + /// with the given radius. + pub fn with_circular_handle(mut self, radius: impl Into) -> Self { + self.handle.shape = HandleShape::Circle { + radius: radius.into().0, + }; + self + } +} + /// The appearance of a slider rail #[derive(Debug, Clone, Copy)] pub struct Rail { @@ -54,15 +65,11 @@ pub enum HandleShape { /// A set of rules that dictate the style of a slider. pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active slider. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of an hovered slider. - fn hovered(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of a slider that is being dragged. - fn dragging(&self, style: &Self::Style) -> Appearance; + fn default() -> fn(&Self, Status) -> Appearance; +} + +pub enum Status { + Active, + Hovered, + Dragging, } diff --git a/style/src/theme.rs b/style/src/theme.rs index 0b56e101..656d6bf9 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -608,88 +608,40 @@ impl container::Appearance> container::StyleSheet for T { } } -/// The style of a slider. -#[derive(Default)] -pub enum Slider { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box>), +impl slider::StyleSheet for Theme { + fn default() -> fn(&Self, slider::Status) -> slider::Appearance { + slider + } } -impl slider::StyleSheet for Theme { - type Style = Slider; +pub fn slider(theme: &Theme, status: slider::Status) -> slider::Appearance { + let palette = theme.extended_palette(); - fn active(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let palette = self.extended_palette(); + let handle = slider::Handle { + shape: slider::HandleShape::Rectangle { + width: 8, + border_radius: 4.0.into(), + }, + color: Color::WHITE, + border_color: Color::WHITE, + border_width: 1.0, + }; - let handle = slider::Handle { - shape: slider::HandleShape::Rectangle { - width: 8, - border_radius: 4.0.into(), - }, - color: Color::WHITE, - border_color: Color::WHITE, - border_width: 1.0, - }; - - slider::Appearance { - rail: slider::Rail { - colors: ( - palette.primary.base.color, - palette.secondary.base.color, - ), - width: 4.0, - border_radius: 2.0.into(), - }, - handle: slider::Handle { - color: palette.background.base.color, - border_color: palette.primary.base.color, - ..handle - }, - } - } - Slider::Custom(custom) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let active = self.active(style); - let palette = self.extended_palette(); - - slider::Appearance { - handle: slider::Handle { - color: palette.primary.weak.color, - ..active.handle - }, - ..active - } - } - Slider::Custom(custom) => custom.hovered(self), - } - } - - fn dragging(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let active = self.active(style); - let palette = self.extended_palette(); - - slider::Appearance { - handle: slider::Handle { - color: palette.primary.base.color, - ..active.handle - }, - ..active - } - } - Slider::Custom(custom) => custom.dragging(self), - } + slider::Appearance { + rail: slider::Rail { + colors: (palette.primary.base.color, palette.secondary.base.color), + width: 4.0, + border_radius: 2.0.into(), + }, + handle: slider::Handle { + color: match status { + slider::Status::Active => palette.background.base.color, + slider::Status::Hovered => palette.primary.weak.color, + slider::Status::Dragging => palette.primary.base.color, + }, + border_color: palette.primary.base.color, + ..handle + }, } } diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 65bc1772..ce02a0a6 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -17,7 +17,7 @@ use crate::core::{ use std::ops::RangeInclusive; pub use iced_style::slider::{ - Appearance, Handle, HandleShape, Rail, StyleSheet, + Appearance, Handle, HandleShape, Rail, Status, StyleSheet, }; /// An horizontal bar and a handle that selects a single value from a range of @@ -58,7 +58,7 @@ where on_release: Option, width: Length, height: f32, - style: Theme::Style, + style: fn(&Theme, Status) -> Appearance, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> @@ -104,7 +104,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style: Default::default(), + style: Theme::default(), } } @@ -140,8 +140,8 @@ where } /// Sets the style of the [`Slider`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; self } @@ -173,7 +173,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size { @@ -203,20 +203,143 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - tree.state.downcast_mut::(), - &mut self.value, - self.default, - &self.range, - self.step, - self.shift_step, - self.on_change.as_ref(), - &self.on_release, - ) + let state = tree.state.downcast_mut::(); + + let is_dragging = state.is_dragging; + let current_value = self.value; + + let locate = |cursor_position: Point| -> Option { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + Some(*self.range.start()) + } else if cursor_position.x >= bounds.x + bounds.width { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + T::from_f64(value) + }; + + new_value + }; + + let increment = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); + + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } + + T::from_f64(new_value) + }; + + let decrement = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); + + if new_value < (*self.range.start()).into() { + return Some(*self.range.start()); + } + + T::from_f64(new_value) + }; + + let change = |new_value: T| { + if (self.value.into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((self.on_change)(new_value)); + + self.value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = + cursor.position_over(layout.bounds()) + { + if state.keyboard_modifiers.command() { + let _ = self.default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(change); + state.is_dragging = true; + } + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = self.on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + let _ = cursor.position().and_then(locate).map(change); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if cursor.position_over(layout.bounds()).is_some() { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } + _ => {} + } + + event::Status::Ignored } fn draw( @@ -229,15 +352,92 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::(), - self.value, - &self.range, + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = (self.style)( theme, - &self.style, + if state.is_dragging { + Status::Dragging + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }, + ); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.height, border_radius), + }; + + let value = self.value.into() as f32; + let (range_start, range_end) = { + let (start, end) = self.range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + let rail_y = bounds.y + bounds.height / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset + handle_width / 2.0, + y: rail_y - style.rail.width / 2.0, + width: bounds.width - offset - handle_width / 2.0, + height: style.rail.width, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset, + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border: Border { + radius: handle_border_radius, + width: style.handle.border_width, + color: style.handle.border_color, + }, + ..renderer::Quad::default() + }, + style.handle.color, ); } @@ -249,7 +449,17 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, tree.state.downcast_ref::()) + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } } } @@ -268,290 +478,8 @@ where } } -/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] -/// accordingly. -pub fn update( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - state: &mut State, - value: &mut T, - default: Option, - range: &RangeInclusive, - step: T, - shift_step: Option, - on_change: &dyn Fn(T) -> Message, - on_release: &Option, -) -> event::Status -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, -{ - let is_dragging = state.is_dragging; - let current_value = *value; - - let locate = |cursor_position: Point| -> Option { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - Some(*range.start()) - } else if cursor_position.x >= bounds.x + bounds.width { - Some(*range.end()) - } else { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let start = (*range.start()).into(); - let end = (*range.end()).into(); - - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - T::from_f64(value) - }; - - new_value - }; - - let increment = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps + 1.0); - - if new_value > (*range.end()).into() { - return Some(*range.end()); - } - - T::from_f64(new_value) - }; - - let decrement = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps - 1.0); - - if new_value < (*range.start()).into() { - return Some(*range.start()); - } - - T::from_f64(new_value) - }; - - let change = |new_value: T| { - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); - - *value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = cursor.position_over(layout.bounds()) - { - if state.keyboard_modifiers.command() { - let _ = default.map(change); - state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; - } - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = on_release.clone() { - shell.publish(on_release); - } - state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - let _ = cursor.position().and_then(locate).map(change); - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { - match key { - Key::Named(key::Named::ArrowUp) => { - let _ = increment(current_value).map(change); - } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws a [`Slider`]. -pub fn draw( - renderer: &mut Renderer, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, - value: T, - range: &RangeInclusive, - theme: &Theme, - style: &Theme::Style, -) where - T: Into + Copy, - Theme: StyleSheet, - Renderer: crate::core::Renderer, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let style = if state.is_dragging { - theme.dragging(style) - } else if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - let (handle_width, handle_height, handle_border_radius) = - match style.handle.shape { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.height, border_radius), - }; - - let value = value.into() as f32; - let (range_start, range_end) = { - let (start, end) = range.clone().into_inner(); - - (start.into() as f32, end.into() as f32) - }; - - let offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) - }; - - let rail_y = bounds.y + bounds.height / 2.0; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y - style.rail.width / 2.0, - width: offset + handle_width / 2.0, - height: style.rail.width, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + offset + handle_width / 2.0, - y: rail_y - style.rail.width / 2.0, - width: bounds.width - offset - handle_width / 2.0, - height: style.rail.width, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + offset, - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - border: Border { - radius: handle_border_radius, - width: style.handle.border_width, - color: style.handle.border_color, - }, - ..renderer::Quad::default() - }, - style.handle.color, - ); -} - -/// Computes the current [`mouse::Interaction`] of a [`Slider`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - } -} - -/// The local state of a [`Slider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { +struct State { is_dragging: bool, keyboard_modifiers: keyboard::Modifiers, } - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 8f7c88da..b6903001 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -3,7 +3,9 @@ //! A [`VerticalSlider`] has some local [`State`]. use std::ops::RangeInclusive; -pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet}; +pub use crate::style::slider::{ + Appearance, Handle, HandleShape, Status, StyleSheet, +}; use crate::core; use crate::core::event::{self, Event}; @@ -55,7 +57,7 @@ where on_release: Option, width: f32, height: Length, - style: Theme::Style, + style: fn(&Theme, Status) -> Appearance, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> @@ -101,7 +103,7 @@ where on_release: None, width: Self::DEFAULT_WIDTH, height: Length::Fill, - style: Default::default(), + style: Theme::default(), } } @@ -137,7 +139,10 @@ where } /// Sets the style of the [`VerticalSlider`]. - pub fn style(mut self, style: impl Into) -> Self { + pub fn style( + mut self, + style: impl Into Appearance>, + ) -> Self { self.style = style.into(); self } @@ -170,7 +175,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size { @@ -200,20 +205,146 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - tree.state.downcast_mut::(), - &mut self.value, - self.default, - &self.range, - self.step, - self.shift_step, - self.on_change.as_ref(), - &self.on_release, - ) + let state = tree.state.downcast_mut::(); + let is_dragging = state.is_dragging; + let current_value = self.value; + + let locate = |cursor_position: Point| -> Option { + let bounds = layout.bounds(); + + let new_value = if cursor_position.y >= bounds.y + bounds.height { + Some(*self.range.start()) + } else if cursor_position.y <= bounds.y { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + T::from_f64(value) + }; + + new_value + }; + + let increment = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); + + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } + + T::from_f64(new_value) + }; + + let decrement = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); + + if new_value < (*self.range.start()).into() { + return Some(*self.range.start()); + } + + T::from_f64(new_value) + }; + + let change = |new_value: T| { + if (self.value.into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((self.on_change)(new_value)); + + self.value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = + cursor.position_over(layout.bounds()) + { + if state.keyboard_modifiers.control() + || state.keyboard_modifiers.command() + { + let _ = self.default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(change); + state.is_dragging = true; + } + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = self.on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + let _ = cursor.position().and_then(locate).map(change); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if cursor.position_over(layout.bounds()).is_some() { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } + _ => {} + } + + event::Status::Ignored } fn draw( @@ -226,15 +357,92 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::(), - self.value, - &self.range, + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = (self.style)( theme, - &self.style, + if state.is_dragging { + Status::Dragging + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }, + ); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.width, border_radius), + }; + + let value = self.value.into() as f32; + let (range_start, range_end) = { + let (start, end) = self.range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.height - handle_width) * (value - range_end) + / (range_start - range_end) + }; + + let rail_x = bounds.x + bounds.width / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: offset + handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y + offset + handle_width / 2.0, + width: style.rail.width, + height: bounds.height - offset - handle_width / 2.0, + }, + border: Border::with_radius(style.rail.border_radius), + ..renderer::Quad::default() + }, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - handle_height / 2.0, + y: bounds.y + offset, + width: handle_height, + height: handle_width, + }, + border: Border { + radius: handle_border_radius, + width: style.handle.border_width, + color: style.handle.border_color, + }, + ..renderer::Quad::default() + }, + style.handle.color, ); } @@ -246,7 +454,17 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, tree.state.downcast_ref::()) + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } } } @@ -266,294 +484,8 @@ where } } -/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`] -/// accordingly. -pub fn update( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - state: &mut State, - value: &mut T, - default: Option, - range: &RangeInclusive, - step: T, - shift_step: Option, - on_change: &dyn Fn(T) -> Message, - on_release: &Option, -) -> event::Status -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, -{ - let is_dragging = state.is_dragging; - let current_value = *value; - - let locate = |cursor_position: Point| -> Option { - let bounds = layout.bounds(); - - let new_value = if cursor_position.y >= bounds.y + bounds.height { - Some(*range.start()) - } else if cursor_position.y <= bounds.y { - Some(*range.end()) - } else { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let start = (*range.start()).into(); - let end = (*range.end()).into(); - - let percent = 1.0 - - f64::from(cursor_position.y - bounds.y) - / f64::from(bounds.height); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - T::from_f64(value) - }; - - new_value - }; - - let increment = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps + 1.0); - - if new_value > (*range.end()).into() { - return Some(*range.end()); - } - - T::from_f64(new_value) - }; - - let decrement = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps - 1.0); - - if new_value < (*range.start()).into() { - return Some(*range.start()); - } - - T::from_f64(new_value) - }; - - let change = |new_value: T| { - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); - - *value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = cursor.position_over(layout.bounds()) - { - if state.keyboard_modifiers.control() - || state.keyboard_modifiers.command() - { - let _ = default.map(change); - state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; - } - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = on_release.clone() { - shell.publish(on_release); - } - state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - let _ = cursor.position().and_then(locate).map(change); - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { - match key { - Key::Named(key::Named::ArrowUp) => { - let _ = increment(current_value).map(change); - } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws a [`VerticalSlider`]. -pub fn draw( - renderer: &mut Renderer, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, - value: T, - range: &RangeInclusive, - style_sheet: &Theme, - style: &Theme::Style, -) where - T: Into + Copy, - Theme: StyleSheet, - Renderer: core::Renderer, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let style = if state.is_dragging { - style_sheet.dragging(style) - } else if is_mouse_over { - style_sheet.hovered(style) - } else { - style_sheet.active(style) - }; - - let (handle_width, handle_height, handle_border_radius) = - match style.handle.shape { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.width, border_radius), - }; - - let value = value.into() as f32; - let (range_start, range_end) = { - let (start, end) = range.clone().into_inner(); - - (start.into() as f32, end.into() as f32) - }; - - let offset = if range_start >= range_end { - 0.0 - } else { - (bounds.height - handle_width) * (value - range_end) - / (range_start - range_end) - }; - - let rail_x = bounds.x + bounds.width / 2.0; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y, - width: style.rail.width, - height: offset + handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y + offset + handle_width / 2.0, - width: style.rail.width, - height: bounds.height - offset - handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - handle_height / 2.0, - y: bounds.y + offset, - width: handle_height, - height: handle_width, - }, - border: Border { - radius: handle_border_radius, - width: style.handle.border_width, - color: style.handle.border_color, - }, - ..renderer::Quad::default() - }, - style.handle.color, - ); -} - -/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - } -} - -/// The local state of a [`VerticalSlider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { +struct State { is_dragging: bool, keyboard_modifiers: keyboard::Modifiers, } - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} From 4130ae4be95ce850263fbc55f490b68a95361d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Mar 2024 19:31:26 +0100 Subject: [PATCH 003/281] Simplify theming for `Text` widget --- core/src/widget/text.rs | 65 ++++++++++++----- examples/integration/src/controls.rs | 103 ++++++++++++--------------- examples/lazy/src/main.rs | 3 +- examples/pane_grid/src/main.rs | 2 +- examples/pokedex/src/main.rs | 6 +- examples/todos/src/main.rs | 6 +- examples/tour/src/main.rs | 2 +- examples/visible_bounds/src/main.rs | 14 ++-- examples/websocket/src/main.rs | 4 +- style/src/theme.rs | 27 +------ widget/src/checkbox.rs | 2 +- widget/src/tooltip.rs | 4 +- 12 files changed, 116 insertions(+), 122 deletions(-) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 0796c4e4..217ad8b3 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -29,7 +29,7 @@ where vertical_alignment: alignment::Vertical, font: Option, shaping: Shaping, - style: Theme::Style, + style: Style, } impl<'a, Theme, Renderer> Text<'a, Theme, Renderer> @@ -49,7 +49,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::Basic, - style: Default::default(), + style: Style::Themed(Theme::default()), } } @@ -74,8 +74,20 @@ where } /// Sets the style of the [`Text`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self { + self.style = Style::Themed(style); + self + } + + /// Sets the [`Color`] of the [`Text`]. + pub fn color(mut self, color: impl Into) -> Self { + self.style = Style::Colored(Some(color.into())); + self + } + + /// Sets the [`Color`] of the [`Text`]. + pub fn color_maybe(mut self, color: Option>) -> Self { + self.style = Style::Colored(color.map(Into::into)); self } @@ -175,14 +187,12 @@ where ) { let state = tree.state.downcast_ref::>(); - draw( - renderer, - style, - layout, - state, - theme.appearance(self.style.clone()), - viewport, - ); + let appearance = match self.style { + Style::Themed(f) => f(theme), + Style::Colored(color) => Appearance { color }, + }; + + draw(renderer, style, layout, state, appearance, viewport); } } @@ -298,7 +308,7 @@ where horizontal_alignment: self.horizontal_alignment, vertical_alignment: self.vertical_alignment, font: self.font, - style: self.style.clone(), + style: self.style, shaping: self.shaping, } } @@ -327,11 +337,18 @@ where /// The style sheet of some text. pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default + Clone; + /// Returns the default styling strategy for [`Text`]. + fn default() -> fn(&Self) -> Appearance { + |_| Appearance::default() + } +} - /// Produces the [`Appearance`] of some text. - fn appearance(&self, style: Self::Style) -> Appearance; +impl StyleSheet for Color { + fn default() -> fn(&Self) -> Appearance { + |color| Appearance { + color: Some(*color), + } + } } /// The apperance of some text. @@ -342,3 +359,17 @@ pub struct Appearance { /// The default, `None`, means using the inherited color. pub color: Option, } + +#[derive(Debug)] +enum Style { + Themed(fn(&Theme) -> Appearance), + Colored(Option), +} + +impl Clone for Style { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Style {} diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index c9bab828..473a7138 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,25 +1,26 @@ use iced_wgpu::Renderer; -use iced_widget::{slider, text_input, Column, Row, Text}; -use iced_winit::core::{Alignment, Color, Element, Length}; +use iced_widget::{column, container, row, slider, text, text_input}; +use iced_winit::core::alignment; +use iced_winit::core::{Color, Element, Length}; use iced_winit::runtime::{Command, Program}; use iced_winit::style::Theme; pub struct Controls { background_color: Color, - text: String, + input: String, } #[derive(Debug, Clone)] pub enum Message { BackgroundColorChanged(Color), - TextChanged(String), + InputChanged(String), } impl Controls { pub fn new() -> Controls { Controls { background_color: Color::BLACK, - text: String::default(), + input: String::default(), } } @@ -38,8 +39,8 @@ impl Program for Controls { Message::BackgroundColorChanged(color) => { self.background_color = color; } - Message::TextChanged(text) => { - self.text = text; + Message::InputChanged(input) => { + self.input = input; } } @@ -48,60 +49,48 @@ impl Program for Controls { fn view(&self) -> Element { let background_color = self.background_color; - let text = &self.text; - let sliders = Row::new() - .width(500) - .spacing(20) - .push( - slider(0.0..=1.0, background_color.r, move |r| { - Message::BackgroundColorChanged(Color { - r, - ..background_color - }) + let sliders = row![ + slider(0.0..=1.0, background_color.r, move |r| { + Message::BackgroundColorChanged(Color { + r, + ..background_color }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.g, move |g| { - Message::BackgroundColorChanged(Color { - g, - ..background_color - }) + }) + .step(0.01), + slider(0.0..=1.0, background_color.g, move |g| { + Message::BackgroundColorChanged(Color { + g, + ..background_color }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.b, move |b| { - Message::BackgroundColorChanged(Color { - b, - ..background_color - }) + }) + .step(0.01), + slider(0.0..=1.0, background_color.b, move |b| { + Message::BackgroundColorChanged(Color { + b, + ..background_color }) - .step(0.01), - ); + }) + .step(0.01), + ] + .width(500) + .spacing(20); - Row::new() - .height(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new().align_items(Alignment::End).push( - Column::new() - .padding(10) - .spacing(10) - .push(Text::new("Background color").style(Color::WHITE)) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ) - .push( - text_input("Placeholder", text) - .on_input(Message::TextChanged), - ), - ), - ) - .into() + container( + column![ + text("Background color").color(Color::WHITE), + text(format!("{background_color:?}")) + .size(14) + .color(Color::WHITE), + text_input("Placeholder", &self.input) + .on_input(Message::InputChanged), + sliders, + ] + .spacing(10), + ) + .padding(10) + .height(Length::Fill) + .align_y(alignment::Vertical::Bottom) + .into() } } diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 9d8c0e35..37b5d52c 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -184,8 +184,7 @@ impl Sandbox for App { .style(theme::Button::Destructive); row![ - text(&item.name) - .style(theme::Text::Color(item.color.into())), + text(&item.name).color(item.color), horizontal_space(), pick_list(Color::ALL, Some(item.color), move |color| { Message::ItemColorChanged(item.clone(), color) diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 39719420..c4bedccc 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -162,7 +162,7 @@ impl Application for Example { let title = row![ pin_button, "Pane", - text(pane.id.to_string()).style(if is_focused { + text(pane.id.to_string()).color(if is_focused { PANE_ID_COLOR_FOCUSED } else { PANE_ID_COLOR_UNFOCUSED diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 8b71a269..193f85f2 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,8 +1,6 @@ use iced::futures; use iced::widget::{self, column, container, image, row, text}; -use iced::{ - Alignment, Application, Color, Command, Element, Length, Settings, Theme, -}; +use iced::{Alignment, Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { Pokedex::run(Settings::default()) @@ -116,7 +114,7 @@ impl Pokemon { text(&self.name).size(30).width(Length::Fill), text(format!("#{}", self.number)) .size(20) - .style(Color::from([0.5, 0.5, 0.5])), + .color([0.5, 0.5, 0.5]), ] .align_items(Alignment::Center) .spacing(20), diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index eae127f7..b1aeb4a7 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -8,7 +8,7 @@ use iced::widget::{ }; use iced::window; use iced::{Application, Element}; -use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced::{Command, Length, Settings, Size, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -209,7 +209,7 @@ impl Application for Todos { let title = text("todos") .width(Length::Fill) .size(100) - .style(Color::from([0.5, 0.5, 0.5])) + .color([0.5, 0.5, 0.5]) .horizontal_alignment(alignment::Horizontal::Center); let input = text_input("What needs to be done?", input_value) @@ -467,7 +467,7 @@ fn empty_message(message: &str) -> Element<'_, Message> { .width(Length::Fill) .size(25) .horizontal_alignment(alignment::Horizontal::Center) - .style(Color::from([0.7, 0.7, 0.7])), + .color([0.7, 0.7, 0.7]), ) .height(200) .center_y() diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 1e2f1ef8..52e1bbb7 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -474,7 +474,7 @@ impl<'a> Step { let color_section = column![ "And its color:", - text(format!("{color:?}")).style(color), + text(format!("{color:?}")).color(color), color_sliders, ] .padding(20) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index bef5d296..10cdc783 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -82,7 +82,10 @@ impl Application for Example { row![ text(label), horizontal_space(), - text(value).font(Font::MONOSPACE).size(14).style(color), + text(value) + .font(Font::MONOSPACE) + .size(14) + .color_maybe(color), ] .height(40) .align_items(Alignment::Center) @@ -102,13 +105,12 @@ impl Application for Example { }) .unwrap_or_default() { - Color { + Some(Color { g: 1.0, ..Color::BLACK - } - .into() + }) } else { - theme::Text::Default + None }, ) }; @@ -120,7 +122,7 @@ impl Application for Example { Some(Point { x, y }) => format!("({x}, {y})"), None => "unknown".to_string(), }, - theme::Text::Default, + None, ), view_bounds("Outer container", self.outer_bounds), view_bounds("Inner container", self.inner_bounds), diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 38a6db1e..47c1898a 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ button, column, container, row, scrollable, text, text_input, }; use iced::{ - Application, Color, Command, Element, Length, Settings, Subscription, Theme, + color, Application, Command, Element, Length, Settings, Subscription, Theme, }; use once_cell::sync::Lazy; @@ -99,7 +99,7 @@ impl Application for WebSocket { let message_log: Element<_> = if self.messages.is_empty() { container( text("Your messages will appear here...") - .style(Color::from_rgb8(0x88, 0x88, 0x88)), + .color(color!(0x888888)), ) .width(Length::Fill) .height(Length::Fill) diff --git a/style/src/theme.rs b/style/src/theme.rs index 656d6bf9..43e7cafd 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -1203,32 +1203,7 @@ impl scrollable::StyleSheet for Theme { } } -/// The style of text. -#[derive(Clone, Copy, Default)] -pub enum Text { - /// The default style. - #[default] - Default, - /// Colored text. - Color(Color), -} - -impl From for Text { - fn from(color: Color) -> Self { - Text::Color(color) - } -} - -impl text::StyleSheet for Theme { - type Style = Text; - - fn appearance(&self, style: Self::Style) -> text::Appearance { - match style { - Text::Default => text::Appearance::default(), - Text::Color(c) => text::Appearance { color: Some(c) }, - } - } -} +impl text::StyleSheet for Theme {} /// The style of a text input. #[derive(Default)] diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 0ff4d58b..3a192fba 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -39,7 +39,7 @@ pub struct Checkbox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + crate::text::StyleSheet, + Theme: StyleSheet, Renderer: text::Renderer, { is_checked: bool, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index d8a1e131..51969aec 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -20,7 +20,7 @@ pub struct Tooltip< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet + crate::text::StyleSheet, + Theme: container::StyleSheet, Renderer: text::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -34,7 +34,7 @@ pub struct Tooltip< impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, + Theme: container::StyleSheet, Renderer: text::Renderer, { /// The default padding of a [`Tooltip`] drawn by this renderer. From db92e1c942154bee474fee5e2c187f8a52a1bb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Mar 2024 19:32:20 +0100 Subject: [PATCH 004/281] Enhnace `Themer` to allow derivation from current `Theme` --- widget/src/helpers.rs | 13 ++--- widget/src/themer.rs | 133 +++++++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index ed385ea5..e6322926 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -15,7 +15,6 @@ use crate::rule::{self, Rule}; use crate::runtime::Command; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; -use crate::style::application; use crate::text::{self, Text}; use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; @@ -440,13 +439,13 @@ where } /// A widget that applies any `Theme` to its contents. -pub fn themer<'a, Message, Theme, Renderer>( - theme: Theme, - content: impl Into>, -) -> Themer<'a, Message, Theme, Renderer> +pub fn themer<'a, Message, OldTheme, NewTheme, F, Renderer>( + to_theme: F, + content: impl Into>, +) -> Themer<'a, Message, OldTheme, NewTheme, F, Renderer> where + F: Fn(&OldTheme) -> NewTheme, Renderer: core::Renderer, - Theme: application::StyleSheet, { - Themer::new(theme, content) + Themer::new(to_theme, content) } diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 3a5fd823..a7eabd2c 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -7,58 +7,68 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, - Size, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Shell, Size, Vector, Widget, }; -use crate::style::application; + +use std::marker::PhantomData; /// A widget that applies any `Theme` to its contents. /// /// This widget can be useful to leverage multiple `Theme` /// types in an application. #[allow(missing_debug_implementations)] -pub struct Themer<'a, Message, Theme, Renderer> +pub struct Themer<'a, Message, Theme, NewTheme, F, Renderer = crate::Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, - Theme: application::StyleSheet, { - content: Element<'a, Message, Theme, Renderer>, - theme: Theme, - style: Theme::Style, - show_background: bool, + content: Element<'a, Message, NewTheme, Renderer>, + to_theme: F, + text_color: Option Color>, + background: Option Background>, + old_theme: PhantomData, } -impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> + Themer<'a, Message, Theme, NewTheme, F, Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, - Theme: application::StyleSheet, { /// Creates an empty [`Themer`] that applies the given `Theme` /// to the provided `content`. - pub fn new(theme: Theme, content: T) -> Self + pub fn new(to_theme: F, content: T) -> Self where - T: Into>, + T: Into>, { Self { content: content.into(), - theme, - style: Theme::Style::default(), - show_background: false, + to_theme, + text_color: None, + background: None, + old_theme: PhantomData, } } - /// Sets whether to draw the background color of the `Theme`. - pub fn background(mut self, background: bool) -> Self { - self.show_background = background; + /// Sets the default text [`Color`] of the [`Themer`]. + pub fn text_color(mut self, f: fn(&NewTheme) -> Color) -> Self { + self.text_color = Some(f); + self + } + + /// Sets the [`Background`] of the [`Themer`]. + pub fn background(mut self, f: fn(&NewTheme) -> Background) -> Self { + self.background = Some(f); self } } -impl<'a, AnyTheme, Message, Theme, Renderer> Widget - for Themer<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> Widget + for Themer<'a, Message, Theme, NewTheme, F, Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, - Theme: application::StyleSheet, { fn tag(&self) -> tree::Tag { self.content.as_widget().tag() @@ -134,38 +144,36 @@ where &self, tree: &Tree, renderer: &mut Renderer, - _theme: &AnyTheme, - _style: &renderer::Style, + theme: &Theme, + style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { - let appearance = self.theme.appearance(&self.style); + let theme = (self.to_theme)(theme); - if self.show_background { + if let Some(background) = self.background { container::draw_background( renderer, &container::Appearance { - background: Some(Background::Color( - appearance.background_color, - )), + background: Some(background(&theme)), ..container::Appearance::default() }, layout.bounds(), ); } - self.content.as_widget().draw( - tree, - renderer, - &self.theme, - &renderer::Style { - text_color: appearance.text_color, - }, - layout, - cursor, - viewport, - ); + let style = if let Some(text_color) = self.text_color { + renderer::Style { + text_color: text_color(&theme), + } + } else { + *style + }; + + self.content + .as_widget() + .draw(tree, renderer, &theme, &style, layout, cursor, viewport); } fn overlay<'b>( @@ -174,15 +182,15 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option> { - struct Overlay<'a, Message, Theme, Renderer> { - theme: &'a Theme, - content: overlay::Element<'a, Message, Theme, Renderer>, + ) -> Option> { + struct Overlay<'a, Message, Theme, NewTheme, Renderer> { + to_theme: &'a dyn Fn(&Theme) -> NewTheme, + content: overlay::Element<'a, Message, NewTheme, Renderer>, } - impl<'a, AnyTheme, Message, Theme, Renderer> - overlay::Overlay - for Overlay<'a, Message, Theme, Renderer> + impl<'a, Message, Theme, NewTheme, Renderer> + overlay::Overlay + for Overlay<'a, Message, Theme, NewTheme, Renderer> where Renderer: crate::core::Renderer, { @@ -197,13 +205,18 @@ where fn draw( &self, renderer: &mut Renderer, - _theme: &AnyTheme, + theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, ) { - self.content - .draw(renderer, self.theme, style, layout, cursor); + self.content.draw( + renderer, + &(self.to_theme)(theme), + style, + layout, + cursor, + ); } fn on_event( @@ -252,12 +265,12 @@ where &'b mut self, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> + ) -> Option> { self.content .overlay(layout, renderer) .map(|content| Overlay { - theme: self.theme, + to_theme: &self.to_theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) @@ -268,24 +281,26 @@ where .as_widget_mut() .overlay(tree, layout, renderer, translation) .map(|content| Overlay { - theme: &self.theme, + to_theme: &self.to_theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) } } -impl<'a, AnyTheme, Message, Theme, Renderer> - From> - for Element<'a, Message, AnyTheme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> + From> + for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + application::StyleSheet, + Theme: 'a, + NewTheme: 'a, + F: Fn(&Theme) -> NewTheme + 'a, Renderer: 'a + crate::core::Renderer, { fn from( - themer: Themer<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, AnyTheme, Renderer> { + themer: Themer<'a, Message, Theme, NewTheme, F, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { Element::new(themer) } } From f4a4845ddbdced81ae4ff60bfa19f0e602d84709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Mar 2024 20:42:37 +0100 Subject: [PATCH 005/281] Simplify theming for `Button` widget --- core/src/background.rs | 13 + core/src/color.rs | 8 + core/src/gradient.rs | 4 +- examples/editor/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 6 +- examples/lazy/src/main.rs | 3 +- examples/pane_grid/src/main.rs | 11 +- examples/screenshot/src/main.rs | 6 +- examples/stopwatch/src/main.rs | 4 +- examples/todos/src/main.rs | 14 +- examples/tour/src/main.rs | 30 ++- style/src/button.rs | 78 ------ style/src/checkbox.rs | 10 +- style/src/theme.rs | 123 +-------- widget/src/button.rs | 404 ++++++++++++++++++------------ widget/src/helpers.rs | 2 +- 16 files changed, 306 insertions(+), 412 deletions(-) diff --git a/core/src/background.rs b/core/src/background.rs index 347c52c0..2e28e560 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -11,6 +11,19 @@ pub enum Background { // TODO: Add image variant } +impl Background { + /// Increases the translucency of the [`Background`] + /// by the given factor. + pub fn transparentize(self, factor: f32) -> Self { + match self { + Self::Color(color) => Self::Color(color.transparentize(factor)), + Self::Gradient(gradient) => { + Self::Gradient(gradient.transparentize(factor)) + } + } + } +} + impl From for Background { fn from(color: Color) -> Self { Background::Color(color) diff --git a/core/src/color.rs b/core/src/color.rs index b8db322f..6526e220 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -151,6 +151,14 @@ impl Color { pub fn inverse(self) -> Color { Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) } + + /// Transparentizes the [`Color`] by the given factor. + pub fn transparentize(self, factor: f32) -> Color { + Self { + a: self.a * factor, + ..self + } + } } impl From<[f32; 3]> for Color { diff --git a/core/src/gradient.rs b/core/src/gradient.rs index 4711b044..ecf7830f 100644 --- a/core/src/gradient.rs +++ b/core/src/gradient.rs @@ -13,11 +13,11 @@ pub enum Gradient { impl Gradient { /// Adjust the opacity of the gradient by a multiplier applied to each color stop. - pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self { + pub fn transparentize(mut self, factor: f32) -> Self { match &mut self { Gradient::Linear(linear) => { for stop in linear.stops.iter_mut().flatten() { - stop.color.a *= alpha_multiplier; + stop.color.a *= factor; } } } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 53c9cf7c..b5870e9e 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -290,7 +290,7 @@ fn action<'a, Message: Clone + 'a>( .style(theme::Container::Box) .into() } else { - action.style(theme::Button::Secondary).into() + action.style(button::secondary).into() } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9cbb7fff..b362381c 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -6,7 +6,6 @@ use grid::Grid; use preset::Preset; use iced::executor; -use iced::theme::{self, Theme}; use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, @@ -14,6 +13,7 @@ use iced::widget::{ use iced::window; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use std::time::Duration; @@ -171,7 +171,7 @@ fn view_controls<'a>( .on_press(Message::TogglePlayback), button("Next") .on_press(Message::Next) - .style(theme::Button::Secondary), + .style(button::secondary), ] .spacing(10); @@ -195,7 +195,7 @@ fn view_controls<'a>( .text_size(16), button("Clear") .on_press(Message::Clear) - .style(theme::Button::Destructive), + .style(button::destructive), ] .padding(10) .spacing(20) diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 37b5d52c..1c5f59d5 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,4 +1,3 @@ -use iced::theme; use iced::widget::{ button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, @@ -181,7 +180,7 @@ impl Sandbox for App { column(items.into_iter().map(|item| { let button = button("Delete") .on_press(Message::DeleteItem(item.clone())) - .style(theme::Button::Destructive); + .style(button::destructive); row![ text(&item.name).color(item.color), diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index c4bedccc..2bed5a03 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,13 +1,13 @@ use iced::alignment::{self, Alignment}; use iced::executor; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{ button, column, container, responsive, row, scrollable, text, }; use iced::{ Application, Color, Command, Element, Length, Settings, Size, Subscription, + Theme, }; pub fn main() -> iced::Result { @@ -287,10 +287,7 @@ fn view_content<'a>( ) ] .push_maybe(if total_panes > 1 && !is_pinned { - Some( - button("Close", Message::Close(pane)) - .style(theme::Button::Destructive), - ) + Some(button("Close", Message::Close(pane)).style(button::destructive)) } else { None }) @@ -327,7 +324,7 @@ fn view_controls<'a>( Some( button(text(content).size(14)) - .style(theme::Button::Secondary) + .style(button::secondary) .padding(3) .on_press(message), ) @@ -336,7 +333,7 @@ fn view_controls<'a>( }); let close = button(text("Close").size(14)) - .style(theme::Button::Destructive) + .style(button::destructive) .padding(3) .on_press_maybe(if total_panes > 1 && !is_pinned { Some(Message::Close(pane)) diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 79749956..dc4684d4 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -216,9 +216,9 @@ impl Application for Example { ) } else { button(centered_text("Saving...")) - .style(theme::Button::Secondary) + .style(button::secondary) } - .style(theme::Button::Secondary) + .style(button::secondary) .padding([10, 20, 10, 20]) .width(Length::Fill) ] @@ -227,7 +227,7 @@ impl Application for Example { crop_controls, button(centered_text("Crop")) .on_press(Message::Crop) - .style(theme::Button::Destructive) + .style(button::destructive) .padding([10, 20, 10, 20]) .width(Length::Fill), ] diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 8a0674c1..7a097e90 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,11 +1,11 @@ use iced::alignment; use iced::executor; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::time; use iced::widget::{button, column, container, row, text}; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use std::time::{Duration, Instant}; @@ -136,7 +136,7 @@ impl Application for Stopwatch { }; let reset_button = button("Reset") - .style(theme::Button::Destructive) + .style(button::destructive) .on_press(Message::Reset); let controls = row![toggle_button, reset_button].spacing(20); diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index b1aeb4a7..b3b5d87a 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,14 +1,14 @@ use iced::alignment::{self, Alignment}; use iced::font::{self, Font}; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::{ self, button, checkbox, column, container, keyed_column, row, scrollable, text, text_input, Text, }; use iced::window; -use iced::{Application, Element}; -use iced::{Command, Length, Settings, Size, Subscription}; +use iced::{ + Application, Command, Element, Length, Settings, Size, Subscription, Theme, +}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -362,7 +362,7 @@ impl Task { button(edit_icon()) .on_press(TaskMessage::Edit) .padding(10) - .style(theme::Button::Text), + .style(button::text), ] .spacing(20) .align_items(Alignment::Center) @@ -385,7 +385,7 @@ impl Task { ) .on_press(TaskMessage::Delete) .padding(10) - .style(theme::Button::Destructive) + .style(button::destructive) ] .spacing(20) .align_items(Alignment::Center) @@ -402,9 +402,9 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { let label = text(label); let button = button(label).style(if filter == current_filter { - theme::Button::Primary + button::primary } else { - theme::Button::Text + button::text }); button.on_press(Message::FilterChanged(filter)).padding(8) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 52e1bbb7..f5791ad7 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,7 +1,6 @@ use iced::alignment::{self, Alignment}; -use iced::theme; use iced::widget::{ - checkbox, column, container, horizontal_space, image, radio, row, + button, checkbox, column, container, horizontal_space, image, radio, row, scrollable, slider, text, text_input, toggler, vertical_space, }; use iced::widget::{Button, Column, Container, Slider}; @@ -56,18 +55,17 @@ impl Sandbox for Tour { fn view(&self) -> Element { let Tour { steps, .. } = self; - let controls = row![] - .push_maybe(steps.has_previous().then(|| { - button("Back") - .on_press(Message::BackPressed) - .style(theme::Button::Secondary) - })) - .push(horizontal_space()) - .push_maybe( - steps - .can_continue() - .then(|| button("Next").on_press(Message::NextPressed)), - ); + let controls = + row![] + .push_maybe(steps.has_previous().then(|| { + padded_button("Back") + .on_press(Message::BackPressed) + .style(button::secondary) + })) + .push(horizontal_space()) + .push_maybe(steps.can_continue().then(|| { + padded_button("Next").on_press(Message::NextPressed) + })); let content: Element<_> = column![ steps.view(self.debug).map(Message::StepMessage), @@ -676,8 +674,8 @@ fn ferris<'a>( .center_x() } -fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { - iced::widget::button(text(label)).padding([12, 24]) +fn padded_button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + button(text(label)).padding([12, 24]) } fn color_slider<'a>( diff --git a/style/src/button.rs b/style/src/button.rs index 0d7a668a..8b137891 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,79 +1 @@ -//! Change the apperance of a button. -use iced_core::{Background, Border, Color, Shadow, Vector}; -/// The appearance of a button. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The amount of offset to apply to the shadow of the button. - pub shadow_offset: Vector, - /// The [`Background`] of the button. - pub background: Option, - /// The text [`Color`] of the button. - pub text_color: Color, - /// The [`Border`] of the buton. - pub border: Border, - /// The [`Shadow`] of the butoon. - pub shadow: Shadow, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - shadow_offset: Vector::default(), - background: None, - text_color: Color::BLACK, - border: Border::default(), - shadow: Shadow::default(), - } - } -} - -/// A set of rules that dictate the style of a button. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a button. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the hovered [`Appearance`] of a button. - fn hovered(&self, style: &Self::Style) -> Appearance { - let active = self.active(style); - - Appearance { - shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), - ..active - } - } - - /// Produces the pressed [`Appearance`] of a button. - fn pressed(&self, style: &Self::Style) -> Appearance { - Appearance { - shadow_offset: Vector::default(), - ..self.active(style) - } - } - - /// Produces the disabled [`Appearance`] of a button. - fn disabled(&self, style: &Self::Style) -> Appearance { - let active = self.active(style); - - Appearance { - shadow_offset: Vector::default(), - background: active.background.map(|background| match background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.mul_alpha(0.5)) - } - }), - text_color: Color { - a: active.text_color.a * 0.5, - ..active.text_color - }, - ..active - } - } -} diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs index 77093f69..5e1c8374 100644 --- a/style/src/checkbox.rs +++ b/style/src/checkbox.rs @@ -30,15 +30,7 @@ pub trait StyleSheet { let active = self.active(style, is_checked); Appearance { - background: match active.background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.mul_alpha(0.5)) - } - }, + background: active.background.transparentize(0.5), ..active } } diff --git a/style/src/theme.rs b/style/src/theme.rs index 43e7cafd..f967aebc 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -4,7 +4,6 @@ pub mod palette; pub use palette::Palette; use crate::application; -use crate::button; use crate::checkbox; use crate::container; use crate::core::widget::text; @@ -22,7 +21,7 @@ use crate::text_editor; use crate::text_input; use crate::toggler; -use crate::core::{Background, Border, Color, Shadow, Vector}; +use crate::core::{Background, Border, Color, Shadow}; use std::fmt; use std::rc::Rc; @@ -285,126 +284,6 @@ impl application::Appearance> application::StyleSheet for T { } } -/// The style of a button. -#[derive(Default)] -pub enum Button { - /// The primary style. - #[default] - Primary, - /// The secondary style. - Secondary, - /// The positive style. - Positive, - /// The destructive style. - Destructive, - /// The text style. - /// - /// Useful for links! - Text, - /// A custom style. - Custom(Box>), -} - -impl Button { - /// Creates a custom [`Button`] style variant. - pub fn custom( - style_sheet: impl button::StyleSheet - - diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c2833210..e1c7d62f 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -18,305 +18,342 @@ use iced_winit::winit; use iced_winit::Clipboard; use winit::{ - event::{Event, WindowEvent}, + event::WindowEvent, event_loop::{ControlFlow, EventLoop}, keyboard::ModifiersState, }; use std::sync::Arc; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsCast; -#[cfg(target_arch = "wasm32")] -use web_sys::HtmlCanvasElement; -#[cfg(target_arch = "wasm32")] -use winit::platform::web::WindowBuilderExtWebSys; - -pub fn main() -> Result<(), Box> { - #[cfg(target_arch = "wasm32")] - let canvas_element = { - console_log::init().expect("Initialize logger"); - - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - - web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.get_element_by_id("iced_canvas")) - .and_then(|element| element.dyn_into::().ok()) - .expect("Get canvas element") - }; - - #[cfg(not(target_arch = "wasm32"))] +pub fn main() -> Result<(), winit::error::EventLoopError> { tracing_subscriber::fmt::init(); // Initialize winit let event_loop = EventLoop::new()?; - #[cfg(target_arch = "wasm32")] - let window = winit::window::WindowBuilder::new() - .with_canvas(Some(canvas_element)) - .build(&event_loop)?; + #[allow(clippy::large_enum_variant)] + enum Runner { + Loading, + Ready { + window: Arc, + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + format: wgpu::TextureFormat, + engine: Engine, + renderer: Renderer, + scene: Scene, + state: program::State, + cursor_position: Option>, + clipboard: Clipboard, + viewport: Viewport, + modifiers: ModifiersState, + resized: bool, + debug: Debug, + }, + } - #[cfg(not(target_arch = "wasm32"))] - let window = winit::window::Window::new(&event_loop)?; + impl winit::application::ApplicationHandler for Runner { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if let Self::Loading = self { + let window = Arc::new( + event_loop + .create_window( + winit::window::WindowAttributes::default(), + ) + .expect("Create window"), + ); - let window = Arc::new(window); + let physical_size = window.inner_size(); + let viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor(), + ); + let clipboard = Clipboard::connect(&window); - let physical_size = window.inner_size(); - let mut viewport = Viewport::with_physical_size( - Size::new(physical_size.width, physical_size.height), - window.scale_factor(), - ); - let mut cursor_position = None; - let mut modifiers = ModifiersState::default(); - let mut clipboard = Clipboard::connect(&window); + let backend = + wgpu::util::backend_bits_from_env().unwrap_or_default(); - // Initialize wgpu - #[cfg(target_arch = "wasm32")] - let default_backend = wgpu::Backends::GL; - #[cfg(not(target_arch = "wasm32"))] - let default_backend = wgpu::Backends::PRIMARY; + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: backend, + ..Default::default() + }); + let surface = instance + .create_surface(window.clone()) + .expect("Create window surface"); - let backend = - wgpu::util::backend_bits_from_env().unwrap_or(default_backend); + let (format, adapter, device, queue) = + futures::futures::executor::block_on(async { + let adapter = + wgpu::util::initialize_adapter_from_env_or_default( + &instance, + Some(&surface), + ) + .await + .expect("Create adapter"); - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: backend, - ..Default::default() - }); - let surface = instance.create_surface(window.clone())?; + let adapter_features = adapter.features(); - let (format, adapter, device, queue) = - futures::futures::executor::block_on(async { - let adapter = wgpu::util::initialize_adapter_from_env_or_default( - &instance, - Some(&surface), - ) - .await - .expect("Create adapter"); + let capabilities = surface.get_capabilities(&adapter); - let adapter_features = adapter.features(); + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: adapter_features + & wgpu::Features::default(), + required_limits: wgpu::Limits::default(), + }, + None, + ) + .await + .expect("Request device"); - #[cfg(target_arch = "wasm32")] - let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()); + ( + capabilities + .formats + .iter() + .copied() + .find(wgpu::TextureFormat::is_srgb) + .or_else(|| { + capabilities.formats.first().copied() + }) + .expect("Get preferred format"), + adapter, + device, + queue, + ) + }); - #[cfg(not(target_arch = "wasm32"))] - let needed_limits = wgpu::Limits::default(); - - let capabilities = surface.get_capabilities(&adapter); - - let (device, queue) = adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - required_features: adapter_features - & wgpu::Features::default(), - required_limits: needed_limits, + surface.configure( + &device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: physical_size.width, + height: physical_size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, }, - None, - ) - .await - .expect("Request device"); + ); - ( - capabilities - .formats - .iter() - .copied() - .find(wgpu::TextureFormat::is_srgb) - .or_else(|| capabilities.formats.first().copied()) - .expect("Get preferred format"), - adapter, + // Initialize scene and GUI controls + let scene = Scene::new(&device, format); + let controls = Controls::new(); + + // Initialize iced + let mut debug = Debug::new(); + let engine = + Engine::new(&adapter, &device, &queue, format, None); + let mut renderer = Renderer::new( + &device, + &engine, + Font::default(), + Pixels::from(16), + ); + + let state = program::State::new( + controls, + viewport.logical_size(), + &mut renderer, + &mut debug, + ); + + // You should change this if you want to render continuously + event_loop.set_control_flow(ControlFlow::Wait); + + *self = Self::Ready { + window, + device, + queue, + surface, + format, + engine, + renderer, + scene, + state, + cursor_position: None, + modifiers: ModifiersState::default(), + clipboard, + viewport, + resized: false, + debug, + }; + } + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Self::Ready { + window, device, queue, - ) - }); + surface, + format, + engine, + renderer, + scene, + state, + viewport, + cursor_position, + modifiers, + clipboard, + resized, + debug, + } = self + else { + return; + }; - surface.configure( - &device, - &wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format, - width: physical_size.width, - height: physical_size.height, - present_mode: wgpu::PresentMode::AutoVsync, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }, - ); + match event { + WindowEvent::RedrawRequested => { + if *resized { + let size = window.inner_size(); - let mut resized = false; - - // Initialize scene and GUI controls - let scene = Scene::new(&device, format); - let controls = Controls::new(); - - // Initialize iced - let mut debug = Debug::new(); - let mut engine = Engine::new(&adapter, &device, &queue, format, None); - let mut renderer = - Renderer::new(&device, &engine, Font::default(), Pixels::from(16)); - - let mut state = program::State::new( - controls, - viewport.logical_size(), - &mut renderer, - &mut debug, - ); - - // Run event loop - event_loop.run(move |event, window_target| { - // You should change this if you want to render continuously - window_target.set_control_flow(ControlFlow::Wait); - - match event { - Event::WindowEvent { - event: WindowEvent::RedrawRequested, - .. - } => { - if resized { - let size = window.inner_size(); - - viewport = Viewport::with_physical_size( - Size::new(size.width, size.height), - window.scale_factor(), - ); - - surface.configure( - &device, - &wgpu::SurfaceConfiguration { - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::AutoVsync, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }, - ); - - resized = false; - } - - match surface.get_current_texture() { - Ok(frame) => { - let mut encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, + *viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor(), ); - let program = state.program(); - - let view = frame.texture.create_view( - &wgpu::TextureViewDescriptor::default(), + surface.configure( + device, + &wgpu::SurfaceConfiguration { + format: *format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }, ); - { - // We clear the frame - let mut render_pass = Scene::clear( - &view, - &mut encoder, - program.background_color(), + *resized = false; + } + + match surface.get_current_texture() { + Ok(frame) => { + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, ); - // Draw the scene - scene.draw(&mut render_pass); + let program = state.program(); + + let view = frame.texture.create_view( + &wgpu::TextureViewDescriptor::default(), + ); + + { + // We clear the frame + let mut render_pass = Scene::clear( + &view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } + + // And then iced on top + renderer.present( + engine, + device, + queue, + &mut encoder, + None, + frame.texture.format(), + &view, + viewport, + &debug.overlay(), + ); + + // Then we submit the work + engine.submit(queue, encoder); + frame.present(); + + // Update the mouse cursor + window.set_cursor( + iced_winit::conversion::mouse_interaction( + state.mouse_interaction(), + ), + ); } - - // And then iced on top - renderer.present( - &mut engine, - &device, - &queue, - &mut encoder, - None, - frame.texture.format(), - &view, - &viewport, - &debug.overlay(), - ); - - // Then we submit the work - queue.submit(Some(encoder.finish())); - frame.present(); - - // Update the mouse cursor - window.set_cursor_icon( - iced_winit::conversion::mouse_interaction( - state.mouse_interaction(), - ), - ); - } - Err(error) => match error { - wgpu::SurfaceError::OutOfMemory => { - panic!( - "Swapchain error: {error}. \ + Err(error) => match error { + wgpu::SurfaceError::OutOfMemory => { + panic!( + "Swapchain error: {error}. \ Rendering cannot continue." + ) + } + _ => { + // Try rendering again next frame. + window.request_redraw(); + } + }, + } + } + WindowEvent::CursorMoved { position, .. } => { + *cursor_position = Some(position); + } + WindowEvent::ModifiersChanged(new_modifiers) => { + *modifiers = new_modifiers.state(); + } + WindowEvent::Resized(_) => { + *resized = true; + } + WindowEvent::CloseRequested => { + event_loop.exit(); + } + _ => {} + } + + // Map window event to iced event + if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, + event, + window.scale_factor(), + *modifiers, + ) { + state.queue_event(event); + } + + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + cursor_position + .map(|p| { + conversion::cursor_position( + p, + viewport.scale_factor(), ) - } - _ => { - // Try rendering again next frame. - window.request_redraw(); - } + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable), + renderer, + &Theme::Dark, + &renderer::Style { + text_color: Color::WHITE, }, - } + clipboard, + debug, + ); + + // and request a redraw + window.request_redraw(); } - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CursorMoved { position, .. } => { - cursor_position = Some(position); - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers.state(); - } - WindowEvent::Resized(_) => { - resized = true; - } - WindowEvent::CloseRequested => { - window_target.exit(); - } - _ => {} - } - - // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( - window::Id::MAIN, - event, - window.scale_factor(), - modifiers, - ) { - state.queue_event(event); - } - } - _ => {} } + } - // If there are events pending - if !state.is_queue_empty() { - // We update iced - let _ = state.update( - viewport.logical_size(), - cursor_position - .map(|p| { - conversion::cursor_position(p, viewport.scale_factor()) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable), - &mut renderer, - &Theme::Dark, - &renderer::Style { - text_color: Color::WHITE, - }, - &mut clipboard, - &mut debug, - ); - - // and request a redraw - window.request_redraw(); - } - })?; - - Ok(()) + let mut runner = Runner::Loading; + event_loop.run_app(&mut runner) } diff --git a/src/application.rs b/src/application.rs index 9197834b..d12ba73d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -212,26 +212,11 @@ where ..crate::graphics::Settings::default() }; - let run = crate::shell::application::run::< + Ok(crate::shell::application::run::< Instance, Self::Executor, ::Compositor, - >(settings.into(), renderer_settings); - - #[cfg(target_arch = "wasm32")] - { - use crate::futures::FutureExt; - use iced_futures::backend::wasm::wasm_bindgen::Executor; - - Executor::new() - .expect("Create Wasm executor") - .spawn(run.map(|_| ())); - - Ok(()) - } - - #[cfg(not(target_arch = "wasm32"))] - Ok(crate::futures::executor::block_on(run)?) + >(settings.into(), renderer_settings)?) } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ad8ac591..1fbdbe9a 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -67,8 +67,6 @@ use crate::core::{ use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; -use std::cell::RefCell; - /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs @@ -84,7 +82,7 @@ pub struct Renderer { // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] - image_cache: RefCell, + image_cache: std::cell::RefCell, } impl Renderer { @@ -103,7 +101,9 @@ impl Renderer { text_storage: text::Storage::new(), #[cfg(any(feature = "svg", feature = "image"))] - image_cache: RefCell::new(_engine.create_image_cache(_device)), + image_cache: std::cell::RefCell::new( + _engine.create_image_cache(_device), + ), } } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index dccb7c07..6d3dddde 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -25,6 +25,7 @@ wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] multi-window = ["iced_runtime/multi-window"] [dependencies] +iced_futures.workspace = true iced_graphics.workspace = true iced_runtime.workspace = true @@ -32,6 +33,7 @@ log.workspace = true rustc-hash.workspace = true thiserror.workspace = true tracing.workspace = true +wasm-bindgen-futures.workspace = true window_clipboard.workspace = true winit.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index a447c9da..e056f4c5 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -22,7 +22,9 @@ use crate::runtime::{Command, Debug}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; +use futures::channel::oneshot; +use std::borrow::Cow; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -129,7 +131,7 @@ pub fn default(theme: &Theme) -> Appearance { /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. -pub async fn run( +pub fn run( settings: Settings, graphics_settings: graphics::Settings, ) -> Result<(), Error> @@ -141,12 +143,12 @@ where { use futures::task; use futures::Future; - use winit::event_loop::EventLoopBuilder; + use winit::event_loop::EventLoop; let mut debug = Debug::new(); debug.startup_started(); - let event_loop = EventLoopBuilder::with_user_event() + let event_loop = EventLoop::with_user_event() .build() .expect("Create event loop"); @@ -165,102 +167,267 @@ where runtime.enter(|| A::new(flags)) }; - #[cfg(target_arch = "wasm32")] - let target = settings.window.platform_specific.target.clone(); + let id = settings.id; + let title = application.title(); - let should_be_visible = settings.window.visible; - let exit_on_close_request = settings.window.exit_on_close_request; + let (boot_sender, boot_receiver) = oneshot::channel(); + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); - let builder = conversion::window_settings( - settings.window, - &application.title(), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); - - log::debug!("Window builder: {builder:#?}"); - - let window = Arc::new( - builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?, - ); - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = window.canvas().expect("Get window canvas"); - let _ = canvas.set_attribute( - "style", - "display: block; width: 100%; height: 100%", - ); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{target}")) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node - .replace_with_with_node_1(&canvas) - .expect(&format!("Could not replace #{}", node.id())); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - } - - let mut compositor = C::new(graphics_settings, window.clone()).await?; - let renderer = compositor.create_renderer(); - - for font in settings.fonts { - compositor.load_font(font); - } - - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); - - let mut instance = Box::pin(run_instance::( + let instance = Box::pin(run_instance::( application, - compositor, - renderer, runtime, proxy, debug, + boot_receiver, event_receiver, control_sender, init_command, - window, - should_be_visible, - exit_on_close_request, + settings.fonts, )); - let mut context = task::Context::from_waker(task::noop_waker_ref()); + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner { + instance: std::pin::Pin>, + context: task::Context<'static>, + boot: Option>, + sender: mpsc::UnboundedSender>, + receiver: mpsc::UnboundedReceiver, + error: Option, + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc>, + #[cfg(target_arch = "wasm32")] + queued_events: Vec>, + } + + struct BootConfig { + sender: oneshot::Sender>, + id: Option, + title: String, + window_settings: window::Settings, + graphics_settings: graphics::Settings, + } + + let runner = Runner { + instance, + context, + boot: Some(BootConfig { + sender: boot_sender, + id, + title, + window_settings: settings.window, + graphics_settings, + }), + sender: event_sender, + receiver: control_receiver, + error: None, + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), + #[cfg(target_arch = "wasm32")] + queued_events: Vec::new(), + }; + + impl winit::application::ApplicationHandler + for Runner + where + F: Future, + C: Compositor + 'static, + { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + let Some(BootConfig { + sender, + id, + title, + window_settings, + graphics_settings, + }) = self.boot.take() + else { + return; + }; + + let should_be_visible = window_settings.visible; + let exit_on_close_request = window_settings.exit_on_close_request; + + #[cfg(target_arch = "wasm32")] + let target = window_settings.platform_specific.target.clone(); + + let window_attributes = conversion::window_attributes( + window_settings, + &title, + event_loop.primary_monitor(), + id, + ) + .with_visible(false); + + log::debug!("Window attributes: {window_attributes:#?}"); + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => Arc::new(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; + } + }; + + let finish_boot = { + let window = window.clone(); + + async move { + let compositor = + C::new(graphics_settings, window.clone()).await?; + + sender + .send(Boot { + window, + compositor, + should_be_visible, + exit_on_close_request, + }) + .ok() + .expect("Send boot event"); + + Ok::<_, graphics::Error>(()) + } + }; + + #[cfg(not(target_arch = "wasm32"))] + if let Err(error) = futures::executor::block_on(finish_boot) { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window.canvas().expect("Get window canvas"); + let _ = canvas.set_attribute( + "style", + "display: block; width: 100%; height: 100%", + ); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!("#{target}")) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node.replace_with_with_node_1(&canvas).expect( + &format!("Could not replace #{}", node.id()), + ); + } + None => { + let _ = body + .append_child(&canvas) + .expect("Append canvas to HTML body"); + } + }; + + let is_booted = self.is_booted.clone(); + + wasm_bindgen_futures::spawn_local(async move { + finish_boot.await.expect("Finish boot!"); + + *is_booted.borrow_mut() = true; + }); + } + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + winit::event::Event::WindowEvent { window_id, event }, + ); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + if is_move_or_resize { + self.process_event( + event_loop, + winit::event::Event::AboutToWait, + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + winit::event::Event::UserEvent(message), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event(event_loop, winit::event::Event::AboutToWait); + } + } + + impl Runner + where + F: Future, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: winit::event::Event, + ) { + // On Wasm, events may start being processed before the compositor + // boots up. We simply queue them and process them once ready. + #[cfg(target_arch = "wasm32")] + if !*self.is_booted.borrow() { + self.queued_events.push(event); + return; + } else if !self.queued_events.is_empty() { + let queued_events = std::mem::take(&mut self.queued_events); + + // This won't infinitely recurse, since we `mem::take` + for event in queued_events { + self.process_event(event_loop, event); + } + } - let process_event = - move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { if event_loop.exiting() { return; } - event_sender.start_send(event).expect("Send event"); + self.sender.start_send(event).expect("Send event"); - let poll = instance.as_mut().poll(&mut context); + let poll = self.instance.as_mut().poll(&mut self.context); match poll { task::Poll::Pending => { - if let Ok(Some(flow)) = control_receiver.try_next() { + if let Ok(Some(flow)) = self.receiver.try_next() { event_loop.set_control_flow(flow); } } @@ -268,54 +435,45 @@ where event_loop.exit(); } } - }; - - #[cfg(not(target_os = "windows"))] - let _ = event_loop.run(process_event); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - let mut process_event = process_event; - - let _ = event_loop.run(move |event, event_loop| { - if matches!( - event, - winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_), - .. - } - ) { - process_event(event, event_loop); - process_event(winit::event::Event::AboutToWait, event_loop); - } else { - process_event(event, event_loop); - } - }); + } } - Ok(()) + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); + + runner.error.map(Err).unwrap_or(Ok(())) + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); + + Ok(()) + } +} + +struct Boot { + window: Arc, + compositor: C, + should_be_visible: bool, + exit_on_close_request: bool, } async fn run_instance( mut application: A, - mut compositor: C, - mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, mut proxy: Proxy, mut debug: Debug, + mut boot: oneshot::Receiver>, mut event_receiver: mpsc::UnboundedReceiver< winit::event::Event, >, mut control_sender: mpsc::UnboundedSender, init_command: Command, - window: Arc, - should_be_visible: bool, - exit_on_close_request: bool, + fonts: Vec>, ) where A: Application + 'static, E: Executor + 'static, @@ -326,6 +484,19 @@ async fn run_instance( use winit::event; use winit::event_loop::ControlFlow; + let Boot { + window, + mut compositor, + should_be_visible, + exit_on_close_request, + } = boot.try_recv().ok().flatten().expect("Receive boot"); + + let mut renderer = compositor.create_renderer(); + + for font in fonts { + compositor.load_font(font); + } + let mut state = State::new(&application, &window); let mut viewport_version = state.viewport_version(); let physical_size = state.physical_size(); @@ -480,7 +651,7 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( + window.set_cursor(conversion::mouse_interaction( new_mouse_interaction, )); diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0f83dac3..d04fc2f1 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -8,16 +8,16 @@ use crate::core::touch; use crate::core::window; use crate::core::{Event, Point, Size}; -/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`. -pub fn window_settings( +/// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`. +pub fn window_attributes( settings: window::Settings, title: &str, primary_monitor: Option, _id: Option, -) -> winit::window::WindowBuilder { - let mut window_builder = winit::window::WindowBuilder::new(); +) -> winit::window::WindowAttributes { + let mut attributes = winit::window::WindowAttributes::default(); - window_builder = window_builder + attributes = attributes .with_title(title) .with_inner_size(winit::dpi::LogicalSize { width: settings.size.width, @@ -39,23 +39,21 @@ pub fn window_settings( if let Some(position) = position(primary_monitor.as_ref(), settings.size, settings.position) { - window_builder = window_builder.with_position(position); + attributes = attributes.with_position(position); } if let Some(min_size) = settings.min_size { - window_builder = - window_builder.with_min_inner_size(winit::dpi::LogicalSize { - width: min_size.width, - height: min_size.height, - }); + attributes = attributes.with_min_inner_size(winit::dpi::LogicalSize { + width: min_size.width, + height: min_size.height, + }); } if let Some(max_size) = settings.max_size { - window_builder = - window_builder.with_max_inner_size(winit::dpi::LogicalSize { - width: max_size.width, - height: max_size.height, - }); + attributes = attributes.with_max_inner_size(winit::dpi::LogicalSize { + width: max_size.width, + height: max_size.height, + }); } #[cfg(any( @@ -65,35 +63,33 @@ pub fn window_settings( target_os = "openbsd" ))] { - // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do - // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. - use ::winit::platform::wayland::WindowBuilderExtWayland; + use ::winit::platform::wayland::WindowAttributesExtWayland; if let Some(id) = _id { - window_builder = window_builder.with_name(id.clone(), id); + attributes = attributes.with_name(id.clone(), id); } } #[cfg(target_os = "windows")] { - use winit::platform::windows::WindowBuilderExtWindows; + use winit::platform::windows::WindowAttributesExtWindows; #[allow(unsafe_code)] unsafe { - window_builder = window_builder + attributes = attributes .with_parent_window(settings.platform_specific.parent); } - window_builder = window_builder + attributes = attributes .with_drag_and_drop(settings.platform_specific.drag_and_drop); - window_builder = window_builder + attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); } #[cfg(target_os = "macos")] { - use winit::platform::macos::WindowBuilderExtMacOS; + use winit::platform::macos::WindowAttributesExtMacOS; - window_builder = window_builder + attributes = attributes .with_title_hidden(settings.platform_specific.title_hidden) .with_titlebar_transparent( settings.platform_specific.titlebar_transparent, @@ -107,25 +103,25 @@ pub fn window_settings( { #[cfg(feature = "x11")] { - use winit::platform::x11::WindowBuilderExtX11; + use winit::platform::x11::WindowAttributesExtX11; - window_builder = window_builder.with_name( + attributes = attributes.with_name( &settings.platform_specific.application_id, &settings.platform_specific.application_id, ); } #[cfg(feature = "wayland")] { - use winit::platform::wayland::WindowBuilderExtWayland; + use winit::platform::wayland::WindowAttributesExtWayland; - window_builder = window_builder.with_name( + attributes = attributes.with_name( &settings.platform_specific.application_id, &settings.platform_specific.application_id, ); } } - window_builder + attributes } /// Converts a winit window event into an iced event. diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 2c148031..145b1569 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -12,6 +12,7 @@ use crate::core::widget::operation; use crate::core::window; use crate::core::{Point, Size}; use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; use crate::futures::futures::executor; use crate::futures::futures::task; use crate::futures::futures::{Future, StreamExt}; @@ -114,12 +115,12 @@ where C: Compositor + 'static, A::Theme: DefaultStyle, { - use winit::event_loop::EventLoopBuilder; + use winit::event_loop::EventLoop; let mut debug = Debug::new(); debug.startup_started(); - let event_loop = EventLoopBuilder::with_user_event() + let event_loop = EventLoop::with_user_event() .build() .expect("Create event loop"); @@ -138,187 +139,277 @@ where runtime.enter(|| A::new(flags)) }; - let should_main_be_visible = settings.window.visible; - let exit_on_close_request = settings.window.exit_on_close_request; + let id = settings.id; + let title = application.title(window::Id::MAIN); - let builder = conversion::window_settings( - settings.window, - &application.title(window::Id::MAIN), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); + let (boot_sender, boot_receiver) = oneshot::channel(); + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); - log::info!("Window builder: {:#?}", builder); - - let main_window = Arc::new( - builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?, - ); - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = main_window.canvas(); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{}", target)) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node - .replace_with_with_node_1(&canvas) - .expect(&format!("Could not replace #{}", node.id())); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - } - - let mut compositor = - executor::block_on(C::new(graphics_settings, main_window.clone()))?; - - let mut window_manager = WindowManager::new(); - let _ = window_manager.insert( - window::Id::MAIN, - main_window, - &application, - &mut compositor, - exit_on_close_request, - ); - - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); - - let mut instance = Box::pin(run_instance::( + let instance = Box::pin(run_instance::( application, - compositor, runtime, proxy, debug, + boot_receiver, event_receiver, control_sender, init_command, - window_manager, - should_main_be_visible, )); - let mut context = task::Context::from_waker(task::noop_waker_ref()); + let context = task::Context::from_waker(task::noop_waker_ref()); - let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { - if event_loop.exiting() { - return; - } - - event_sender - .start_send(Event::EventLoopAwakened(event)) - .expect("Send event"); - - loop { - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => match control_receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if new < current => {} - ( - ControlFlow::WaitUntil(target), - ControlFlow::Wait, - ) if target > Instant::now() => {} - _ => { - event_loop.set_control_flow(flow); - } - } - } - Control::CreateWindow { - id, - settings, - title, - monitor, - } => { - let exit_on_close_request = - settings.exit_on_close_request; - - let window = conversion::window_settings( - settings, &title, monitor, None, - ) - .build(event_loop) - .expect("Failed to build window"); - - event_sender - .start_send(Event::WindowCreated { - id, - window, - exit_on_close_request, - }) - .expect("Send event"); - } - Control::Exit => { - event_loop.exit(); - } - }, - _ => { - break; - } - }, - task::Poll::Ready(_) => { - event_loop.exit(); - break; - } - }; - } - }; - - #[cfg(not(target_os = "windows"))] - let _ = event_loop.run(process_event); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - let mut process_event = process_event; - - let _ = event_loop.run(move |event, event_loop| { - if matches!( - event, - winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_), - .. - } - ) { - process_event(event, event_loop); - process_event(winit::event::Event::AboutToWait, event_loop); - } else { - process_event(event, event_loop); - } - }); + struct Runner { + instance: std::pin::Pin>, + context: task::Context<'static>, + boot: Option>, + sender: mpsc::UnboundedSender>, + receiver: mpsc::UnboundedReceiver, + error: Option, } + struct BootConfig { + sender: oneshot::Sender>, + id: Option, + title: String, + window_settings: window::Settings, + graphics_settings: graphics::Settings, + } + + let mut runner = Runner { + instance, + context, + boot: Some(BootConfig { + sender: boot_sender, + id, + title, + window_settings: settings.window, + graphics_settings, + }), + sender: event_sender, + receiver: control_receiver, + error: None, + }; + + impl winit::application::ApplicationHandler + for Runner + where + F: Future, + C: Compositor, + { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + let Some(BootConfig { + sender, + id, + title, + window_settings, + graphics_settings, + }) = self.boot.take() + else { + return; + }; + + let should_be_visible = window_settings.visible; + let exit_on_close_request = window_settings.exit_on_close_request; + + let window_attributes = conversion::window_attributes( + window_settings, + &title, + event_loop.primary_monitor(), + id, + ) + .with_visible(false); + + log::debug!("Window attributes: {window_attributes:#?}"); + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => Arc::new(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; + } + }; + + let finish_boot = async move { + let compositor = + C::new(graphics_settings, window.clone()).await?; + + sender + .send(Boot { + window, + compositor, + should_be_visible, + exit_on_close_request, + }) + .ok() + .expect("Send boot event"); + + Ok::<_, graphics::Error>(()) + }; + + if let Err(error) = executor::block_on(finish_boot) { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::WindowEvent { + window_id, + event, + }), + ); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + if is_move_or_resize { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::AboutToWait, + ), + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::UserEvent( + message, + )), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); + } + } + + impl Runner + where + F: Future, + C: Compositor, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event, + ) { + if event_loop.exiting() { + return; + } + + self.sender.start_send(event).expect("Send event"); + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if new < current => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = event_loop + .create_window( + conversion::window_attributes( + settings, &title, monitor, None, + ), + ) + .expect("Create window"); + + self.process_event( + event_loop, + Event::WindowCreated { + id, + window, + exit_on_close_request, + }, + ); + } + Control::Exit => { + event_loop.exit(); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + } + } + + let _ = event_loop.run_app(&mut runner); + Ok(()) } +struct Boot { + window: Arc, + compositor: C, + should_be_visible: bool, + exit_on_close_request: bool, +} + enum Event { WindowCreated { id: window::Id, @@ -341,15 +432,13 @@ enum Control { async fn run_instance( mut application: A, - mut compositor: C, mut runtime: Runtime, A::Message>, mut proxy: Proxy, mut debug: Debug, + mut boot: oneshot::Receiver>, mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, init_command: Command, - mut window_manager: WindowManager, - should_main_window_be_visible: bool, ) where A: Application + 'static, E: Executor + 'static, @@ -359,11 +448,28 @@ async fn run_instance( use winit::event; use winit::event_loop::ControlFlow; + let Boot { + window: main_window, + mut compositor, + should_be_visible, + exit_on_close_request, + } = boot.try_recv().ok().flatten().expect("Receive boot"); + + let mut window_manager = WindowManager::new(); + + let _ = window_manager.insert( + window::Id::MAIN, + main_window.clone(), + &application, + &mut compositor, + exit_on_close_request, + ); + let main_window = window_manager .get_mut(window::Id::MAIN) .expect("Get main window"); - if should_main_window_be_visible { + if should_be_visible { main_window.raw.set_visible(true); } @@ -532,7 +638,7 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor_icon( + window.raw.set_cursor( conversion::mouse_interaction( new_mouse_interaction, ), @@ -603,7 +709,7 @@ async fn run_instance( if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor_icon( + window.raw.set_cursor( conversion::mouse_interaction( new_mouse_interaction, ), diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 3a0c8556..57a7dc7e 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -9,8 +9,9 @@ use std::sync::Arc; use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] -pub struct WindowManager +pub struct WindowManager where + A: Application, C: Compositor, A::Theme: DefaultStyle, { From 7e7285d60f08e9d32a796e9adf7e37535a67fc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 17:00:55 +0200 Subject: [PATCH 272/281] Plug `new_events` handler to event loop --- winit/src/application.rs | 15 +++++++++++++++ winit/src/multi_window.rs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/winit/src/application.rs b/winit/src/application.rs index e056f4c5..3bc29255 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -341,6 +341,21 @@ where } } + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + winit::event::Event::NewEvents(cause), + ); + } + fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 145b1569..673a6f30 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -254,6 +254,21 @@ where } } + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), + ); + } + fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, From 8a0701a0d95989769341846b05ffcc705ae4ee5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 21:05:29 +0200 Subject: [PATCH 273/281] Introduce `ICED_PRESENT_MODE` env var to pick a `wgpu::PresentMode` --- wgpu/src/settings.rs | 14 ++++++++++++++ wgpu/src/window/compositor.rs | 25 +++++++++++++++---------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index a6aea0a5..5e2603ee 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -51,3 +51,17 @@ impl From for Settings { } } } + +pub fn present_mode_from_env() -> Option { + let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?; + + match present_mode.to_lowercase().as_str() { + "vsync" => Some(wgpu::PresentMode::AutoVsync), + "no_vsync" => Some(wgpu::PresentMode::AutoNoVsync), + "immediate" => Some(wgpu::PresentMode::Immediate), + "fifo" => Some(wgpu::PresentMode::Fifo), + "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed), + "mailbox" => Some(wgpu::PresentMode::Mailbox), + _ => None, + } +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index baf9f315..2e938c77 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -4,7 +4,8 @@ use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::error; use crate::graphics::{self, Viewport}; -use crate::{Engine, Renderer, Settings}; +use crate::settings::{self, Settings}; +use crate::{Engine, Renderer}; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] @@ -270,15 +271,19 @@ impl graphics::Compositor for Compositor { backend: Option<&str>, ) -> Result { match backend { - None | Some("wgpu") => Ok(new( - Settings { - backends: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::all()), - ..settings.into() - }, - compatible_window, - ) - .await?), + None | Some("wgpu") => { + let mut settings = Settings::from(settings); + + if let Some(backends) = wgpu::util::backend_bits_from_env() { + settings.backends = backends; + } + + if let Some(present_mode) = settings::present_mode_from_env() { + settings.present_mode = present_mode; + } + + Ok(new(settings, compatible_window).await?) + } Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound { backend: "wgpu", reason: error::Reason::DidNotMatch { From 5b6f3499e114c1694a5878466f8d46e7022e1bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 21:13:51 +0200 Subject: [PATCH 274/281] Document `present_mode_from_env` in `iced_wgpu` --- wgpu/src/settings.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 5e2603ee..b3c3cf6a 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -52,6 +52,18 @@ impl From for Settings { } } +/// Obtains a [`wgpu::PresentMode`] from the current environment +/// configuration, if set. +/// +/// The value returned by this function can be changed by setting +/// the `ICED_PRESENT_MODE` env variable. The possible values are: +/// +/// - `vsync` → [`wgpu::PresentMode::AutoVsync`] +/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`] +/// - `immediate` → [`wgpu::PresentMode::Immediate`] +/// - `fifo` → [`wgpu::PresentMode::Fifo`] +/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`] +/// - `mailbox` → [`wgpu::PresentMode::Mailbox`] pub fn present_mode_from_env() -> Option { let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?; From 247870f83bc12f3f4b549bb582f1058f5adcce25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 7 May 2024 21:38:31 +0200 Subject: [PATCH 275/281] Redirect `docs.iced.rs` to actual docs --- .github/workflows/document.yml | 2 ++ docs/redirect.html | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/redirect.html diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index ba482215..827a2ca8 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -27,6 +27,8 @@ jobs: -p iced - name: Write CNAME file run: echo 'docs.iced.rs' > ./target/doc/CNAME + - name: Copy redirect file as index.html + run: cp docs/redirect.html target/doc/index.html - name: Publish documentation if: github.ref == 'refs/heads/master' uses: peaceiris/actions-gh-pages@v3 diff --git a/docs/redirect.html b/docs/redirect.html new file mode 100644 index 00000000..7b2cef51 --- /dev/null +++ b/docs/redirect.html @@ -0,0 +1,13 @@ + + + + + + + Redirecting... + + + +

If you are not redirected automatically, follow this link.

+ + From 447f3a2d14ab1c4bc20e232df1aa2375623af2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 May 2024 12:29:17 +0200 Subject: [PATCH 276/281] Reuse `glyphon::Pipeline` state in `iced_wgpu` --- Cargo.toml | 2 +- wgpu/src/text.rs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 06f7d198..4f50f018 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "ceed55403ce53e120ce9d1fae17dcfe388726118" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "cd66a24859cf30b0b8cabf06256dacad362ed44a" } guillotiere = "0.6" half = "2.2" image = "0.24" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 7e683c77..68741461 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -115,6 +115,7 @@ impl Storage { queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, format: wgpu::TextureFormat, + pipeline: &glyphon::Pipeline, cache: &Cache, new_transformation: Transformation, bounds: Rectangle, @@ -131,7 +132,7 @@ impl Storage { Group { atlas: glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, + device, queue, pipeline, format, COLOR_MODE, ), version: 0, should_trim: false, @@ -259,6 +260,7 @@ impl Storage { #[allow(missing_debug_implementations)] pub struct Pipeline { + state: glyphon::Pipeline, format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, renderers: Vec, @@ -272,12 +274,16 @@ impl Pipeline { queue: &wgpu::Queue, format: wgpu::TextureFormat, ) -> Self { + let state = glyphon::Pipeline::new(device); + let atlas = glyphon::TextAtlas::with_color_mode( + device, queue, &state, format, COLOR_MODE, + ); + Pipeline { + state, format, renderers: Vec::new(), - atlas: glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, - ), + atlas, prepare_layer: 0, cache: BufferCache::new(), } @@ -343,6 +349,7 @@ impl Pipeline { queue, encoder, self.format, + &self.state, cache, layer_transformation * *transformation, layer_bounds * layer_transformation, From bed53f81436c595e479009427e1fa4ff4a1048d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 May 2024 13:22:22 +0200 Subject: [PATCH 277/281] Reuse `glyphon::Viewport` explicitly --- Cargo.toml | 2 +- wgpu/src/lib.rs | 13 +++++++++---- wgpu/src/text.rs | 42 +++++++++++++++++++++++++++++------------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f50f018..1bd26ea6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "cd66a24859cf30b0b8cabf06256dacad362ed44a" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f410f3ab4fc7b484003684881124cb0ee7ef2e01" } guillotiere = "0.6" half = "2.2" image = "0.24" diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 1fbdbe9a..ad88ce3e 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -79,6 +79,7 @@ pub struct Renderer { triangle_storage: triangle::Storage, text_storage: text::Storage, + text_viewport: text::Viewport, // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] @@ -87,8 +88,8 @@ pub struct Renderer { impl Renderer { pub fn new( - _device: &wgpu::Device, - _engine: &Engine, + device: &wgpu::Device, + engine: &Engine, default_font: Font, default_text_size: Pixels, ) -> Self { @@ -99,10 +100,11 @@ impl Renderer { triangle_storage: triangle::Storage::new(), text_storage: text::Storage::new(), + text_viewport: engine.text_pipeline.create_viewport(device), #[cfg(any(feature = "svg", feature = "image"))] image_cache: std::cell::RefCell::new( - _engine.create_image_cache(_device), + engine.create_image_cache(device), ), } } @@ -141,6 +143,8 @@ impl Renderer { ) { let scale_factor = viewport.scale_factor() as f32; + self.text_viewport.update(queue, viewport.physical_size()); + for layer in self.layers.iter_mut() { if !layer.quads.is_empty() { engine.quad_pipeline.prepare( @@ -182,12 +186,12 @@ impl Renderer { engine.text_pipeline.prepare( device, queue, + &self.text_viewport, encoder, &mut self.text_storage, &layer.text, layer.bounds, Transformation::scale(scale_factor), - viewport.physical_size(), ); } @@ -357,6 +361,7 @@ impl Renderer { if !layer.text.is_empty() { text_layer += engine.text_pipeline.render( + &self.text_viewport, &self.text_storage, text_layer, &layer.text, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 68741461..9ecd5c99 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -113,13 +113,13 @@ impl Storage { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, format: wgpu::TextureFormat, pipeline: &glyphon::Pipeline, cache: &Cache, new_transformation: Transformation, bounds: Rectangle, - target_size: Size, ) { let group_count = self.groups.len(); @@ -152,6 +152,7 @@ impl Storage { let _ = prepare( device, queue, + viewport, encoder, &mut upload.renderer, &mut group.atlas, @@ -159,7 +160,6 @@ impl Storage { &cache.text, bounds, new_transformation, - target_size, ); } @@ -189,6 +189,7 @@ impl Storage { let _ = prepare( device, queue, + viewport, encoder, &mut renderer, &mut group.atlas, @@ -196,7 +197,6 @@ impl Storage { &cache.text, bounds, new_transformation, - target_size, ); } @@ -258,6 +258,20 @@ impl Storage { } } +pub struct Viewport(glyphon::Viewport); + +impl Viewport { + pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size) { + self.0.update( + queue, + glyphon::Resolution { + width: resolution.width, + height: resolution.height, + }, + ); + } +} + #[allow(missing_debug_implementations)] pub struct Pipeline { state: glyphon::Pipeline, @@ -293,12 +307,12 @@ impl Pipeline { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &Viewport, encoder: &mut wgpu::CommandEncoder, storage: &mut Storage, batch: &Batch, layer_bounds: Rectangle, layer_transformation: Transformation, - target_size: Size, ) { for item in batch { match item { @@ -319,6 +333,7 @@ impl Pipeline { let result = prepare( device, queue, + &viewport.0, encoder, renderer, &mut self.atlas, @@ -326,7 +341,6 @@ impl Pipeline { text, layer_bounds * layer_transformation, layer_transformation * *transformation, - target_size, ); match result { @@ -347,13 +361,13 @@ impl Pipeline { storage.prepare( device, queue, + &viewport.0, encoder, self.format, &self.state, cache, layer_transformation * *transformation, layer_bounds * layer_transformation, - target_size, ); } } @@ -362,6 +376,7 @@ impl Pipeline { pub fn render<'a>( &'a self, + viewport: &'a Viewport, storage: &'a Storage, start: usize, batch: &'a Batch, @@ -383,7 +398,7 @@ impl Pipeline { let renderer = &self.renderers[start + layer_count]; renderer - .render(&self.atlas, render_pass) + .render(&self.atlas, &viewport.0, render_pass) .expect("Render text"); layer_count += 1; @@ -392,7 +407,7 @@ impl Pipeline { if let Some((atlas, upload)) = storage.get(cache) { upload .renderer - .render(atlas, render_pass) + .render(atlas, &viewport.0, render_pass) .expect("Render cached text"); } } @@ -402,6 +417,10 @@ impl Pipeline { layer_count } + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(glyphon::Viewport::new(device, &self.state)) + } + pub fn end_frame(&mut self) { self.atlas.trim(); self.cache.trim(); @@ -413,6 +432,7 @@ impl Pipeline { fn prepare( device: &wgpu::Device, queue: &wgpu::Queue, + viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, renderer: &mut glyphon::TextRenderer, atlas: &mut glyphon::TextAtlas, @@ -420,7 +440,6 @@ fn prepare( sections: &[Text], layer_bounds: Rectangle, layer_transformation: Transformation, - target_size: Size, ) -> Result<(), glyphon::PrepareError> { let mut font_system = font_system().write().expect("Write font system"); let font_system = font_system.raw(); @@ -617,10 +636,7 @@ fn prepare( encoder, font_system, atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, + viewport, text_areas, &mut glyphon::SwashCache::new(), ) From 99c1464cc16979381b3a531d293ee34fd6697480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 May 2024 19:34:43 +0200 Subject: [PATCH 278/281] Update `glyphon` fork to a cleaner branch --- Cargo.toml | 2 +- wgpu/src/text.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1bd26ea6..fc35fee8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f410f3ab4fc7b484003684881124cb0ee7ef2e01" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" } guillotiere = "0.6" half = "2.2" image = "0.24" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 9ecd5c99..05db5f80 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -116,7 +116,7 @@ impl Storage { viewport: &glyphon::Viewport, encoder: &mut wgpu::CommandEncoder, format: wgpu::TextureFormat, - pipeline: &glyphon::Pipeline, + state: &glyphon::Cache, cache: &Cache, new_transformation: Transformation, bounds: Rectangle, @@ -132,7 +132,7 @@ impl Storage { Group { atlas: glyphon::TextAtlas::with_color_mode( - device, queue, pipeline, format, COLOR_MODE, + device, queue, state, format, COLOR_MODE, ), version: 0, should_trim: false, @@ -274,7 +274,7 @@ impl Viewport { #[allow(missing_debug_implementations)] pub struct Pipeline { - state: glyphon::Pipeline, + state: glyphon::Cache, format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, renderers: Vec, @@ -288,7 +288,7 @@ impl Pipeline { queue: &wgpu::Queue, format: wgpu::TextureFormat, ) -> Self { - let state = glyphon::Pipeline::new(device); + let state = glyphon::Cache::new(device); let atlas = glyphon::TextAtlas::with_color_mode( device, queue, &state, format, COLOR_MODE, ); From fb5f1ba0bc728069b63d299e98411fad483b4177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 May 2024 20:29:27 +0200 Subject: [PATCH 279/281] Create dynamic text `wgpu` benchmark --- benches/ipsum.txt | 9 +++++++ benches/wgpu.rs | 63 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 benches/ipsum.txt diff --git a/benches/ipsum.txt b/benches/ipsum.txt new file mode 100644 index 00000000..3e2d6396 --- /dev/null +++ b/benches/ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at elit mollis, dictum nunc non, tempus metus. Sed iaculis ac mauris eu lobortis. Integer elementum venenatis eros, id placerat odio feugiat vel. Maecenas consequat convallis tincidunt. Nunc eu lorem justo. Praesent quis ornare sapien. Aliquam interdum tortor ut rhoncus faucibus. Suspendisse molestie scelerisque nulla, eget sodales lacus sodales vel. Nunc placerat id arcu sodales venenatis. Praesent ullamcorper viverra nibh eget efficitur. Aliquam molestie felis vehicula, finibus sapien eget, accumsan purus. Praesent vestibulum eleifend consectetur. Sed tincidunt lectus a libero efficitur, non scelerisque lectus tincidunt. + +Cras ullamcorper tincidunt tellus non tempor. Integer pulvinar turpis quam, nec pharetra purus egestas non. Vivamus sed ipsum consequat, dignissim ante et, suscipit nibh. Quisque et mauris eu erat rutrum cursus. Pellentesque ut neque eu neque eleifend auctor ac hendrerit dolor. Morbi eget egestas ex. Integer hendrerit ipsum in enim bibendum, at vehicula ipsum dapibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempus consectetur tortor, vel fermentum sem pulvinar eget. Maecenas rutrum fringilla eros a pellentesque. Cras quis magna consectetur, tristique massa vel, aliquet nunc. Aliquam erat volutpat. Suspendisse porttitor risus id auctor fermentum. Vivamus efficitur tellus sed tortor cursus tincidunt. Sed auctor varius arcu, non congue tellus vehicula finibus. + +Fusce a tincidunt urna. Nunc at quam ac enim tempor vehicula imperdiet in sapien. Donec lobortis tristique felis vel semper. Quisque vulputate felis eu enim vestibulum malesuada. Fusce a lobortis mauris, iaculis eleifend ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus sodales vel elit dignissim mattis. + +Aliquam placerat vulputate dignissim. Proin pellentesque vitae arcu ut feugiat. Nunc mi felis, ornare at gravida sed, vestibulum sed urna. Duis fermentum maximus viverra. Donec imperdiet pellentesque sollicitudin. Cras non sem quis metus bibendum molestie. Duis imperdiet nec lectus eu rutrum. Mauris congue enim purus, in iaculis arcu dapibus ut. Nullam id erat tincidunt, iaculis dolor non, lobortis magna. Proin convallis scelerisque maximus. Morbi at lorem fringilla libero blandit fringilla. Ut aliquet tellus non sem dictum viverra. Aenean venenatis purus eget lacus placerat, non mollis mauris pellentesque. + +Etiam elit diam, aliquet quis suscipit non, condimentum viverra odio. Praesent mi enim, suscipit id mi in, rhoncus ultricies lorem. Nulla facilisi. Integer convallis sagittis euismod. Vestibulum porttitor sodales turpis ac accumsan. Nullam molestie turpis vel lacus tincidunt, sed finibus erat pharetra. Nullam vestibulum turpis id sollicitudin accumsan. Praesent eget posuere lacus. Donec vehicula, nisl nec suscipit porta, felis lorem gravida orci, a hendrerit tellus nibh sit amet elit. diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 2d308666..02c7b1f9 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use iced::alignment; use iced::mouse; -use iced::widget::{canvas, stack, text}; +use iced::widget::{canvas, scrollable, stack, text}; use iced::{ Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, }; @@ -14,20 +14,31 @@ criterion_group!(benches, wgpu_benchmark); #[allow(unused_results)] pub fn wgpu_benchmark(c: &mut Criterion) { - c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); - c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); + c.bench_function("wgpu — canvas (light)", |b| { + benchmark(b, |_| scene(10)) + }); + c.bench_function("wgpu — canvas (heavy)", |b| { + benchmark(b, |_| scene(1_000)) + }); c.bench_function("wgpu - layered text (light)", |b| { - benchmark(b, layered_text(10)); + benchmark(b, |_| layered_text(10)); }); c.bench_function("wgpu - layered text (heavy)", |b| { - benchmark(b, layered_text(1_000)); + benchmark(b, |_| layered_text(1_000)); + }); + + c.bench_function("wgpu - dynamic text (light)", |b| { + benchmark(b, |i| dynamic_text(1_000, i)); + }); + c.bench_function("wgpu - dynamic text (heavy)", |b| { + benchmark(b, |i| dynamic_text(100_000, i)); }); } -fn benchmark( +fn benchmark<'a>( bencher: &mut Bencher<'_>, - widget: Element<'_, (), Theme, Renderer>, + view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>, ) { use iced_futures::futures::executor; use iced_wgpu::graphics; @@ -94,14 +105,17 @@ fn benchmark( let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut user_interface = runtime::UserInterface::build( - widget, - viewport.logical_size(), - runtime::user_interface::Cache::default(), - &mut renderer, - ); + let mut i = 0; + let mut cache = Some(runtime::user_interface::Cache::default()); bencher.iter(|| { + let mut user_interface = runtime::UserInterface::build( + view(i), + viewport.logical_size(), + cache.take().unwrap(), + &mut renderer, + ); + let _ = user_interface.draw( &mut renderer, &Theme::Dark, @@ -111,6 +125,8 @@ fn benchmark( mouse::Cursor::Unavailable, ); + cache = Some(user_interface.into_cache()); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None, @@ -130,6 +146,8 @@ fn benchmark( let submission = engine.submit(&queue, encoder); let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); + + i += 1; }); } @@ -189,3 +207,22 @@ fn layered_text<'a, Message: 'a>( .height(Length::Fill) .into() } + +fn dynamic_text<'a, Message: 'a>( + n: usize, + i: usize, +) -> Element<'a, Message, Theme, Renderer> { + const LOREM_IPSUM: &'static str = include_str!("ipsum.txt"); + + scrollable( + text(format!( + "{}... Iteration {i}", + std::iter::repeat(LOREM_IPSUM.chars()) + .flatten() + .take(n) + .collect::(), + )) + .size(10), + ) + .into() +} From 7e7af91e6f7dc187e8fb95746d8811289a088217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 May 2024 20:34:31 +0200 Subject: [PATCH 280/281] Fix `clippy` lints in `wgpu` benchmark --- benches/wgpu.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 02c7b1f9..0e407253 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -15,10 +15,10 @@ criterion_group!(benches, wgpu_benchmark); #[allow(unused_results)] pub fn wgpu_benchmark(c: &mut Criterion) { c.bench_function("wgpu — canvas (light)", |b| { - benchmark(b, |_| scene(10)) + benchmark(b, |_| scene(10)); }); c.bench_function("wgpu — canvas (heavy)", |b| { - benchmark(b, |_| scene(1_000)) + benchmark(b, |_| scene(1_000)); }); c.bench_function("wgpu - layered text (light)", |b| { @@ -212,7 +212,7 @@ fn dynamic_text<'a, Message: 'a>( n: usize, i: usize, ) -> Element<'a, Message, Theme, Renderer> { - const LOREM_IPSUM: &'static str = include_str!("ipsum.txt"); + const LOREM_IPSUM: &str = include_str!("ipsum.txt"); scrollable( text(format!( From 718fe5b7de6c705b1797c49d382182182acdbe80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 May 2024 23:14:37 +0200 Subject: [PATCH 281/281] Pass `WindowHandle` by value to `window::run_with_handle` --- runtime/src/window.rs | 2 +- runtime/src/window/action.rs | 2 +- winit/src/application.rs | 2 +- winit/src/multi_window.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 24171e3e..e32465d3 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -197,7 +197,7 @@ pub fn change_icon(id: Id, icon: Icon) -> Command { /// Note that if the window closes before this call is processed the callback will not be run. pub fn run_with_handle( id: Id, - f: impl FnOnce(&WindowHandle<'_>) -> Message + 'static, + f: impl FnOnce(WindowHandle<'_>) -> Message + 'static, ) -> Command { Command::single(command::Action::Window(Action::RunWithHandle( id, diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index e44ff5a6..07e77872 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -106,7 +106,7 @@ pub enum Action { /// said, it's usually in the same ballpark as on Windows. ChangeIcon(Id, Icon), /// Runs the closure with the native window handle of the window with the given [`Id`]. - RunWithHandle(Id, Box) -> T + 'static>), + RunWithHandle(Id, Box) -> T + 'static>), /// Screenshot the viewport of the window. Screenshot(Id, Box T + 'static>), } diff --git a/winit/src/application.rs b/winit/src/application.rs index 3bc29255..f7508b4c 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1050,7 +1050,7 @@ pub fn run_command( use window::raw_window_handle::HasWindowHandle; if let Ok(handle) = window.window_handle() { - proxy.send(tag(&handle)); + proxy.send(tag(handle)); } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 673a6f30..4cc08d18 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1223,7 +1223,7 @@ fn run_command( .get_mut(id) .and_then(|window| window.raw.window_handle().ok()) { - proxy.send(tag(&handle)); + proxy.send(tag(handle)); } } window::Action::Screenshot(id, tag) => {