From 15230cbee361496c39be9197af408a1024dddfed Mon Sep 17 00:00:00 2001 From: Craig Watson Date: Fri, 23 May 2025 12:16:29 -0700 Subject: [PATCH 01/25] implement Debug for Task --- runtime/src/task.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 756f592b..f6cc961d 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -17,7 +17,6 @@ pub use sipper::{Never, Sender, Sipper, Straw, sipper, stream}; /// A set of concurrent actions to be performed by the iced runtime. /// /// A [`Task`] _may_ produce a bunch of values of type `T`. -#[allow(missing_debug_implementations)] #[must_use = "`Task` must be returned to the runtime to take effect; normally in your `update` or `new` functions."] pub struct Task { stream: Option>>, @@ -278,6 +277,17 @@ impl Task { } } +impl std::fmt::Debug for Task { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Task<{}>, units={}", + std::any::type_name::(), + self.units + ) + } +} + /// A handle to a [`Task`] that can be used for aborting it. #[derive(Debug, Clone)] pub struct Handle { From 7eb371729be57f8c3e7d3c519651066892f84964 Mon Sep 17 00:00:00 2001 From: Craig Watson Date: Tue, 27 May 2025 08:40:34 -0700 Subject: [PATCH 02/25] Remove unnecessary bound --- runtime/src/task.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index f6cc961d..a932a162 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -277,7 +277,7 @@ impl Task { } } -impl std::fmt::Debug for Task { +impl std::fmt::Debug for Task { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, From c1ab6263073b7e6f1195969df359d4bf9cb169df Mon Sep 17 00:00:00 2001 From: edwloef Date: Thu, 30 Jan 2025 15:00:33 +0100 Subject: [PATCH 03/25] fix rectangle snapping --- core/src/point.rs | 10 ++++++++++ core/src/rectangle.rs | 11 +++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/src/point.rs b/core/src/point.rs index cea57518..510f0d86 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -107,3 +107,13 @@ where write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y) } } + +impl Point { + /// Snaps the [`Point`] to __unsigned__ integer coordinates. + pub fn snap(self) -> Point { + Point { + x: self.x.round() as u32, + y: self.y.round() as u32, + } + } +} diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 0c4add05..44c29287 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -243,16 +243,19 @@ impl Rectangle { /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. pub fn snap(self) -> Option> { - let width = self.width as u32; - let height = self.height as u32; + let top_left = self.position().snap(); + let bottom_right = (self.position() + self.size().into()).snap(); + + let width = bottom_right.x - top_left.x; + let height = bottom_right.y - top_left.y; if width < 1 || height < 1 { return None; } Some(Rectangle { - x: self.x as u32, - y: self.y as u32, + x: top_left.x, + y: top_left.y, width, height, }) From 6f935f3449a3cfe7a64a8454a295688d41f2e733 Mon Sep 17 00:00:00 2001 From: edwloef Date: Thu, 29 May 2025 23:55:05 +0200 Subject: [PATCH 04/25] fix underflow when calculating new bounds --- core/src/rectangle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 44c29287..8ffb0797 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -246,8 +246,8 @@ impl Rectangle { let top_left = self.position().snap(); let bottom_right = (self.position() + self.size().into()).snap(); - let width = bottom_right.x - top_left.x; - let height = bottom_right.y - top_left.y; + let width = bottom_right.x.checked_sub(top_left.x)?; + let height = bottom_right.y.checked_sub(top_left.y)?; if width < 1 || height < 1 { return None; From 3f7ceb8be69bb56bffcaa0224d779246b7493d41 Mon Sep 17 00:00:00 2001 From: Johann Birnick Date: Thu, 29 May 2025 21:08:45 -0700 Subject: [PATCH 05/25] fix docs --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e9ca3a04..b7f0cfd0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -659,8 +659,8 @@ pub type Element< /// The result of running an iced program. pub type Result = std::result::Result<(), Error>; -/// Runs a basic iced application with default [`Settings`] given its title, -/// update, and view logic. +/// Runs a basic iced application with default [`Settings`] given its update +/// and view logic. /// /// This is equivalent to chaining [`application()`] with [`Application::run`]. /// From 54a9f1b77029798789b448c087a940816c975b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 30 May 2025 18:09:40 +0200 Subject: [PATCH 06/25] Snap `rule` widget to the pixel grid --- widget/src/rule.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 65c0a6dc..b109b103 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -167,6 +167,7 @@ where renderer::Quad { bounds, border: border::rounded(style.radius), + snap: true, ..renderer::Quad::default() }, style.color, From 7afbb89ebfd49aa86453d45f7d9c13a7c48b58f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 30 May 2025 18:11:38 +0200 Subject: [PATCH 07/25] Add `snap` field to `rule::Style` --- widget/src/rule.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/widget/src/rule.rs b/widget/src/rule.rs index b109b103..03c57c94 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -167,7 +167,7 @@ where renderer::Quad { bounds, border: border::rounded(style.radius), - snap: true, + snap: style.snap, ..renderer::Quad::default() }, style.color, @@ -198,6 +198,8 @@ pub struct Style { pub radius: border::Radius, /// The [`FillMode`] of the rule. pub fill_mode: FillMode, + /// Whether the rule should be snapped to the pixel grid. + pub snap: bool, } /// The fill mode of a rule. @@ -302,5 +304,6 @@ pub fn default(theme: &Theme) -> Style { width: 1, radius: 0.0.into(), fill_mode: FillMode::Full, + snap: true, } } From f19c45494a51db73588fad16933c8be5e8078fe2 Mon Sep 17 00:00:00 2001 From: DKolter Date: Sun, 1 Jun 2025 12:45:05 +0200 Subject: [PATCH 08/25] fix: Wgpu text bounds cutoff --- wgpu/src/text.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index f2908e71..d979b625 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -628,8 +628,8 @@ fn prepare( bounds: cryoglyph::TextBounds { left: clip_bounds.x as i32, top: clip_bounds.y as i32, - right: (clip_bounds.x + clip_bounds.width) as i32, - bottom: (clip_bounds.y + clip_bounds.height) as i32, + right: (clip_bounds.x + clip_bounds.width).round() as i32, + bottom: (clip_bounds.y + clip_bounds.height).round() as i32, }, default_color: to_color(color), }) From dcf5abb0d8c724e68b916cd9dd17c37b26a3cb84 Mon Sep 17 00:00:00 2001 From: DKolter Date: Tue, 3 Jun 2025 12:17:26 +0200 Subject: [PATCH 09/25] Also add round to text bound position --- wgpu/src/text.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index d979b625..6fcc8e45 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -626,8 +626,8 @@ fn prepare( scale: transformation.scale_factor() * layer_transformation.scale_factor(), bounds: cryoglyph::TextBounds { - left: clip_bounds.x as i32, - top: clip_bounds.y as i32, + left: clip_bounds.x.round() as i32, + top: clip_bounds.y.round() as i32, right: (clip_bounds.x + clip_bounds.width).round() as i32, bottom: (clip_bounds.y + clip_bounds.height).round() as i32, }, From 8efda5e68835dfc192f60143b16e5c82dab37c5f Mon Sep 17 00:00:00 2001 From: Craig Watson Date: Tue, 3 Jun 2025 09:28:15 -0700 Subject: [PATCH 10/25] use `debug_struct` instead of `write!` --- runtime/src/task.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index a932a162..731c0be8 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -279,12 +279,9 @@ impl Task { impl std::fmt::Debug for Task { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Task<{}>, units={}", - std::any::type_name::(), - self.units - ) + f.debug_struct(&format!("Task<{}>", std::any::type_name::())) + .field("units", &self.units) + .finish() } } From 08a37406f2d2fa6c8aacb6131e0675e7f12cda35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 4 Jun 2025 12:14:31 +0200 Subject: [PATCH 11/25] Replace generic `into` call with `Vector::from` --- core/src/rectangle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 8ffb0797..bea27b9d 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -244,7 +244,7 @@ impl Rectangle { /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. pub fn snap(self) -> Option> { let top_left = self.position().snap(); - let bottom_right = (self.position() + self.size().into()).snap(); + let bottom_right = (self.position() + Vector::from(self.size())).snap(); let width = bottom_right.x.checked_sub(top_left.x)?; let height = bottom_right.y.checked_sub(top_left.y)?; From bde4572bc5bc1178bb3dfabc86f545989bb7229a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Jun 2025 14:06:19 +0200 Subject: [PATCH 12/25] Remove `PartialEq` implementation for `window::Position` --- core/src/window/position.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/window/position.rs b/core/src/window/position.rs index 1c8e86b6..2b31a5e2 100644 --- a/core/src/window/position.rs +++ b/core/src/window/position.rs @@ -1,7 +1,7 @@ use crate::{Point, Size}; /// The position of a window in a given screen. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy)] pub enum Position { /// The platform-specific default position for a new window. Default, From 5ca5000cdc370598c9cbd6f74e53f307df4edea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Jun 2025 14:29:36 +0200 Subject: [PATCH 13/25] Add elided lifetimes to view helpers in examples --- examples/arc/src/main.rs | 2 +- examples/bezier_tool/src/main.rs | 2 +- examples/changelog/src/main.rs | 2 +- examples/checkbox/src/main.rs | 2 +- examples/clock/src/main.rs | 2 +- examples/color_palette/src/main.rs | 6 +++--- examples/combo_box/src/main.rs | 2 +- examples/counter/src/main.rs | 2 +- examples/custom_quad/src/main.rs | 2 +- examples/custom_widget/src/main.rs | 2 +- examples/download_progress/src/main.rs | 4 ++-- examples/editor/src/main.rs | 2 +- examples/events/src/main.rs | 2 +- examples/exit/src/main.rs | 2 +- examples/ferris/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 4 ++-- examples/gradient/src/main.rs | 2 +- examples/integration/src/controls.rs | 2 +- examples/layout/src/main.rs | 12 +++++++++--- examples/lazy/src/main.rs | 2 +- examples/loading_spinners/src/main.rs | 2 +- examples/loupe/src/main.rs | 2 +- examples/markdown/overview.md | 2 +- examples/markdown/src/main.rs | 2 +- examples/modal/src/main.rs | 2 +- examples/multi_window/src/main.rs | 4 ++-- examples/multitouch/src/main.rs | 2 +- examples/pane_grid/src/main.rs | 2 +- examples/pick_list/src/main.rs | 2 +- examples/pokedex/src/main.rs | 4 ++-- examples/progress_bar/src/main.rs | 2 +- examples/qr_code/src/main.rs | 2 +- examples/scrollable/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 6 ++++-- examples/slider/src/main.rs | 2 +- examples/solar_system/src/main.rs | 2 +- examples/stopwatch/src/main.rs | 2 +- examples/styling/src/main.rs | 2 +- examples/svg/src/main.rs | 2 +- examples/system_information/src/main.rs | 2 +- examples/the_matrix/src/main.rs | 2 +- examples/todos/src/main.rs | 11 +++++++---- examples/tooltip/src/main.rs | 2 +- examples/tour/src/main.rs | 24 ++++++++++++------------ examples/url_handler/src/main.rs | 2 +- examples/vectorial_text/src/main.rs | 2 +- examples/visible_bounds/src/main.rs | 2 +- examples/websocket/src/main.rs | 2 +- src/lib.rs | 22 +++++++++++----------- widget/src/markdown.rs | 4 ++-- 50 files changed, 95 insertions(+), 84 deletions(-) diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 3e80a5db..ce461ab6 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -36,7 +36,7 @@ impl Arc { self.cache.clear(); } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { Canvas::new(self).width(Fill).height(Fill).into() } diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 113577e6..8b040755 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -34,7 +34,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { container(hover( self.bezier.view(&self.curves).map(Message::AddCurve), if self.curves.is_empty() { diff --git a/examples/changelog/src/main.rs b/examples/changelog/src/main.rs index fbb0cfbf..ba254c76 100644 --- a/examples/changelog/src/main.rs +++ b/examples/changelog/src/main.rs @@ -217,7 +217,7 @@ impl Generator { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { match self { Self::Loading => center("Loading...").into(), Self::Done => center( diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index 563b721d..99c859a7 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -38,7 +38,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let default_checkbox = checkbox("Default", self.default) .on_toggle(Message::DefaultToggled); diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 89855bc2..428b1e63 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -48,7 +48,7 @@ impl Clock { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let canvas = canvas(self as &Self).width(Fill).height(Fill); container(canvas).padding(20).into() diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 10e72d3b..e5554dee 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -56,7 +56,7 @@ impl ColorPalette { self.theme = Theme::new(to_color(srgb)); } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let base = self.theme.base; let srgb = to_rgb(base); @@ -149,7 +149,7 @@ impl Theme { .chain(self.higher.iter()) } - pub fn view(&self) -> Element { + pub fn view(&self) -> Element<'_, Message> { Canvas::new(self).width(Fill).height(Fill).into() } @@ -296,7 +296,7 @@ trait ColorSpace: Sized { } impl ColorPicker { - fn view(&self, color: C) -> Element { + fn view(&self, color: C) -> Element<'_, C> { let [c1, c2, c3] = color.components(); let [cr1, cr2, cr3] = C::COMPONENT_RANGES; diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index 5124c8ef..a7a68590 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -47,7 +47,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let combo_box = combo_box( &self.languages, "Type a language...", diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 1bb17af5..5027afd7 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -28,7 +28,7 @@ impl Counter { } } - fn view(&self) -> Column { + fn view(&self) -> Column<'_, Message> { column![ button("Increment").on_press(Message::Increment), text(self.value).size(50), diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index b23b8e96..4ea58511 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -74,7 +74,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let border::Radius { top_left, top_right, diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index db4255be..e5db6139 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -103,7 +103,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let content = column![ circle(self.radius), text!("Radius: {:.2}", self.radius), diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index c27c6204..53ee2823 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -61,7 +61,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let downloads = Column::with_children(self.downloads.iter().map(Download::view)) .push( @@ -152,7 +152,7 @@ impl Download { } } - pub fn view(&self) -> Element { + pub fn view(&self) -> Element<'_, Message> { let current_progress = match &self.state { State::Idle => 0.0, State::Downloading { progress, .. } => *progress, diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index af75f581..abad1abe 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -144,7 +144,7 @@ impl Editor { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let controls = row![ action(new_icon(), "New file", Some(Message::NewFile)), action( diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index c38b22b9..57b36f7c 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -55,7 +55,7 @@ impl Events { event::listen().map(Message::EventOccurred) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let events = Column::with_children( self.last .iter() diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 6d1b5c52..767ff93f 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -29,7 +29,7 @@ impl Exit { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let content = if self.show_confirm { column![ "Are you sure you want to exit?", diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index c28b04c6..add266b5 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -91,7 +91,7 @@ impl Image { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let i_am_ferris = column![ "Hello!", Element::from( diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8860759e..e6cb4dab 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -111,7 +111,7 @@ impl GameOfLife { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let version = self.version; let selected_speed = self.next_speed.unwrap_or(self.speed); let controls = view_controls( @@ -320,7 +320,7 @@ mod grid { } } - pub fn view(&self) -> Element { + pub fn view(&self) -> Element<'_, Message> { Canvas::new(self).width(Fill).height(Fill).into() } diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 93c40e05..8c0c90d3 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -51,7 +51,7 @@ impl Gradient { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let Self { start, end, diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 72db4c24..1f2f3805 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -38,7 +38,7 @@ impl Controls { } } - pub fn view(&self) -> Element { + pub fn view(&self) -> Element<'_, Message, Theme, Renderer> { let background_color = self.background_color; let sliders = row![ diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 768180a8..f477c406 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -68,7 +68,7 @@ impl Layout { }) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let header = row![ text(self.example.title).size(20).font(Font::MONOSPACE), horizontal_space(), @@ -121,7 +121,7 @@ impl Layout { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Eq)] struct Example { title: &'static str, view: fn() -> Element<'static, Message>, @@ -190,7 +190,7 @@ impl Example { Self::LIST.get(index + 1).copied().unwrap_or(self) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { (self.view)() } } @@ -201,6 +201,12 @@ impl Default for Example { } } +impl PartialEq for Example { + fn eq(&self, other: &Self) -> bool { + self.title == other.title + } +} + fn centered<'a>() -> Element<'a, Message> { center(text("I am centered!").size(50)).into() } diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 3d2eb61c..585b8c0a 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -154,7 +154,7 @@ impl App { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let options = lazy(self.version, |_| { let mut items: Vec<_> = self.items.iter().cloned().collect(); diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 0cec5095..122b30fd 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -37,7 +37,7 @@ impl LoadingSpinners { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let column = [ &easing::EMPHASIZED, &easing::EMPHASIZED_DECELERATE, diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 114f481d..c141e758 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -30,7 +30,7 @@ impl Loupe { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { center(loupe( 3.0, column![ diff --git a/examples/markdown/overview.md b/examples/markdown/overview.md index 66336c5b..2bb2d931 100644 --- a/examples/markdown/overview.md +++ b/examples/markdown/overview.md @@ -34,7 +34,7 @@ Now, let's show the actual counter by putting it all together in our __view logi use iced::widget::{button, column, text, Column}; impl Counter { - pub fn view(&self) -> Column { + pub fn view(&self) -> Column<'_, Message> { // We use a column: a simple vertical layout column![ // The increment button. We tell it to produce an diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 12d52dff..98d19595 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -174,7 +174,7 @@ impl Markdown { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let editor = text_editor(&self.raw) .placeholder("Type your Markdown here...") .on_action(Message::Edit) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 236f32f8..b4355004 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -92,7 +92,7 @@ impl App { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let content = container( column![ row![text("Top Left"), horizontal_space(), text("Top Right")] diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 4470c834..ff20c468 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -130,7 +130,7 @@ impl Example { } } - fn view(&self, window_id: window::Id) -> Element { + fn view(&self, window_id: window::Id) -> Element<'_, Message> { if let Some(window) = self.windows.get(&window_id) { center(window.view(window_id)).into() } else { @@ -168,7 +168,7 @@ impl Window { } } - fn view(&self, id: window::Id) -> Element { + fn view(&self, id: window::Id) -> Element<'_, Message> { let scale_input = column![ text("Window scale factor:"), text_input("Window Scale", &self.scale_input) diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 0db1c09a..6ea20511 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -43,7 +43,7 @@ impl Multitouch { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { Canvas::new(self).width(Fill).height(Fill).into() } } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index aad55122..c44fc1f1 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -129,7 +129,7 @@ impl Example { }) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let focus = self.focus; let total_panes = self.panes.len(); diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 33aa6cda..1023a30a 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -24,7 +24,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let pick_list = pick_list( &Language::ALL[..], self.selected_language, diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 8fe23717..d56c0878 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -63,7 +63,7 @@ impl Pokedex { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let content: Element<_> = match self { Pokedex::Loading => { text("Searching for Pokémon...").size(40).into() @@ -100,7 +100,7 @@ struct Pokemon { impl Pokemon { const TOTAL: u16 = 807; - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { row![ image::viewer(self.image.clone()), column![ diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index df1c4dc2..8e43f090 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -30,7 +30,7 @@ impl Progress { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let bar = progress_bar(0.0..=100.0, self.value); column![ diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 91625c95..20217615 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -63,7 +63,7 @@ impl QRGenerator { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let title = text("QR Code Generator").size(70); let input = diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 793aae2a..087f8e51 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -118,7 +118,7 @@ impl ScrollableDemo { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let scrollbar_width_slider = slider( 0..=15, self.scrollbar_width, diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index f83ee5fd..f6911c29 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -1,7 +1,9 @@ use iced::mouse; use iced::widget::canvas::{self, Canvas, Event, Geometry}; use iced::widget::{column, row, slider, text}; -use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme}; +use iced::{ + Center, Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, +}; use rand::Rng; use std::fmt::Debug; @@ -46,7 +48,7 @@ impl SierpinskiEmulator { self.graph.redraw(); } - fn view(&self) -> iced::Element<'_, Message> { + fn view(&self) -> Element<'_, Message> { column![ Canvas::new(&self.graph).width(Fill).height(Fill), row![ diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index ea11aad9..eff453c6 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -27,7 +27,7 @@ impl Slider { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let h_slider = container( slider(1..=100, self.value, Message::SliderChanged) .default(50) diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 90545e66..958fc672 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -55,7 +55,7 @@ impl SolarSystem { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { canvas(&self.state).width(Fill).height(Fill).into() } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index e4d5d7d0..9488f55a 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -83,7 +83,7 @@ impl Stopwatch { Subscription::batch(vec![tick, keyboard::on_key_press(handle_hotkey)]) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { const MINUTE: u64 = 60; const HOUR: u64 = 60 * MINUTE; diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index b3386d35..b78e04b9 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -65,7 +65,7 @@ impl Styling { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let choose_theme = column![ text("Theme:"), pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged) diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index ce580ca1..53b064d5 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -24,7 +24,7 @@ impl Tiger { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let handle = svg::Handle::from_path(format!( "{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR") diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index f0bdb32d..61bca4c8 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -47,7 +47,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { use bytesize::ByteSize; let content: Element<_> = match self { diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 2f520c27..6bf3447d 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -34,7 +34,7 @@ impl TheMatrix { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { canvas(self).width(Fill).height(Fill).into() } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 8a4bc2eb..65f4a56c 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -182,7 +182,7 @@ impl Todos { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { match self { Todos::Loading => loading_message(), Todos::Loaded(State { @@ -334,7 +334,7 @@ impl Task { } } - fn view(&self, i: usize) -> Element { + fn view(&self, i: usize) -> Element<'_, TaskMessage> { match &self.state { TaskState::Idle => { let checkbox = checkbox(&self.description, self.completed) @@ -381,7 +381,10 @@ impl Task { } } -fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { +fn view_controls( + tasks: &[Task], + current_filter: Filter, +) -> Element<'_, Message> { let tasks_left = tasks.iter().filter(|task| !task.completed).count(); let filter_button = |label, filter, current_filter| { @@ -580,7 +583,7 @@ mod tests { use iced_test::selector::id; use iced_test::{Error, Simulator}; - fn simulator(todos: &Todos) -> Simulator { + fn simulator(todos: &Todos) -> Simulator<'_, Message> { Simulator::with_settings( Settings { fonts: vec![Todos::ICON_FONT.into()], diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index 599fcd4e..bf927271 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -33,7 +33,7 @@ impl Tooltip { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let tooltip = tooltip( button("Press to change position") .on_press(Message::ChangePosition), diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 5d31cbd7..a08c96f3 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -141,7 +141,7 @@ impl Tour { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let controls = row![] .push_maybe(self.screen.previous().is_some().then(|| { @@ -197,7 +197,7 @@ impl Tour { } } - fn welcome(&self) -> Column { + fn welcome(&self) -> Column<'_, Message> { Self::container("Welcome!") .push( "This is a simple tour meant to showcase a bunch of \ @@ -244,7 +244,7 @@ impl Tour { ) } - fn slider(&self) -> Column { + fn slider(&self) -> Column<'_, Message> { Self::container("Slider") .push( "A slider allows you to smoothly select a value from a range \ @@ -258,7 +258,7 @@ impl Tour { .push(text(self.slider.to_string()).width(Fill).align_x(Center)) } - fn rows_and_columns(&self) -> Column { + fn rows_and_columns(&self) -> Column<'_, Message> { let row_radio = radio( "Row", Layout::Row, @@ -303,7 +303,7 @@ impl Tour { .push(spacing_section) } - fn text(&self) -> Column { + fn text(&self) -> Column<'_, Message> { let size = self.text_size; let color = self.text_color; @@ -339,7 +339,7 @@ impl Tour { .push(color_section) } - fn radio(&self) -> Column { + fn radio(&self) -> Column<'_, Message> { let question = column![ text("Iced is written in...").size(24), column( @@ -374,7 +374,7 @@ impl Tour { ) } - fn toggler(&self) -> Column { + fn toggler(&self) -> Column<'_, Message> { Self::container("Toggler") .push("A toggler is mostly used to enable or disable something.") .push( @@ -387,7 +387,7 @@ impl Tour { ) } - fn image(&self) -> Column { + fn image(&self) -> Column<'_, Message> { let width = self.image_width; let filter_method = self.image_filter_method; @@ -406,7 +406,7 @@ impl Tour { .align_x(Center) } - fn scrollable(&self) -> Column { + fn scrollable(&self) -> Column<'_, Message> { Self::container("Scrollable") .push( "Iced supports scrollable content. Try it out! Find the \ @@ -428,7 +428,7 @@ impl Tour { .push(text("You made it!").width(Fill).size(50).align_x(Center)) } - fn text_input(&self) -> Column { + fn text_input(&self) -> Column<'_, Message> { let value = &self.input_value; let is_secure = self.input_is_secure; let is_showing_icon = self.input_is_showing_icon; @@ -474,7 +474,7 @@ impl Tour { ) } - fn debugger(&self) -> Column { + fn debugger(&self) -> Column<'_, Message> { Self::container("Debugger") .push( "You can ask Iced to visually explain the layouting of the \ @@ -491,7 +491,7 @@ impl Tour { .push("Feel free to go back and take a look.") } - fn end(&self) -> Column { + fn end(&self) -> Column<'_, Message> { Self::container("You reached the end!") .push("This tour will be updated as more features are added.") .push("Make sure to keep an eye on it!") diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 7c3e3f35..1bc295c9 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -31,7 +31,7 @@ impl App { event::listen_url().map(Message::UrlReceived) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let content = match &self.url { Some(url) => text(url), None => text("No URL received yet!"), diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 72525d55..d0ba91b9 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -48,7 +48,7 @@ impl VectorialText { self.state.cache.clear(); } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let slider_with_label = |label, range, value, message: fn(f32) -> _| { column![ row![text(label), horizontal_space(), text!("{:.2}", value)], diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 0b152e44..8e5e4a07 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -59,7 +59,7 @@ impl Example { } } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let data_row = |label, value, color| { row![ text(label), diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index c52de37e..95f06fa4 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -90,7 +90,7 @@ impl WebSocket { Subscription::run(echo::connect).map(Message::Echo) } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Message> { let message_log: Element<_> = if self.messages.is_empty() { center( text("Your messages will appear here...") diff --git a/src/lib.rs b/src/lib.rs index b7f0cfd0..a933d21f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ //! iced::run(update, view) //! } //! # fn update(state: &mut (), message: ()) {} -//! # fn view(state: &()) -> iced::Element<()> { iced::widget::text("").into() } +//! # fn view(state: &()) -> iced::Element<'_, ()> { iced::widget::text("").into() } //! ``` //! //! Define an `update` function to __change__ your state: @@ -55,7 +55,7 @@ //! use iced::widget::{button, text}; //! use iced::Element; //! -//! fn view(counter: &u64) -> Element { +//! fn view(counter: &u64) -> Element<'_, Message> { //! button(text(counter)).on_press(Message::Increment).into() //! } //! # #[derive(Clone)] @@ -95,7 +95,7 @@ //! } //! } //! -//! fn view(counter: &Counter) -> Element { +//! fn view(counter: &Counter) -> Element<'_, Message> { //! button(text(counter.value)).on_press(Message::Increment).into() //! } //! ``` @@ -115,7 +115,7 @@ //! use iced::widget::{button, column, text}; //! use iced::Element; //! -//! fn view(counter: &Counter) -> Element { +//! fn view(counter: &Counter) -> Element<'_, Message> { //! column![ //! text(counter.value).size(20), //! button("Increment").on_press(Message::Increment), @@ -144,7 +144,7 @@ //! use iced::widget::{column, container, row}; //! use iced::{Fill, Element}; //! -//! fn view(state: &State) -> Element { +//! fn view(state: &State) -> Element<'_, Message> { //! container( //! column![ //! "Top", @@ -187,7 +187,7 @@ //! use iced::widget::container; //! use iced::Element; //! -//! fn view(state: &State) -> Element { +//! fn view(state: &State) -> Element<'_, Message> { //! container("I am 300px tall!").height(300).into() //! } //! ``` @@ -216,7 +216,7 @@ //! Theme::TokyoNight //! } //! # fn update(state: &mut State, message: ()) {} -//! # fn view(state: &State) -> iced::Element<()> { iced::widget::text("").into() } +//! # fn view(state: &State) -> iced::Element<'_, ()> { iced::widget::text("").into() } //! ``` //! //! The `theme` function takes the current state of the application, allowing the @@ -237,7 +237,7 @@ //! use iced::widget::container; //! use iced::Element; //! -//! fn view(state: &State) -> Element { +//! fn view(state: &State) -> Element<'_, Message> { //! container("I am a rounded box!").style(container::rounded_box).into() //! } //! ``` @@ -252,7 +252,7 @@ //! use iced::widget::button; //! use iced::{Element, Theme}; //! -//! fn view(state: &State) -> Element { +//! fn view(state: &State) -> Element<'_, Message> { //! button("I am a styled button!").style(|theme: &Theme, status| { //! let palette = theme.extended_palette(); //! @@ -359,7 +359,7 @@ //! } //! # fn new() -> State { State } //! # fn update(state: &mut State, message: Message) {} -//! # fn view(state: &State) -> iced::Element { iced::widget::text("").into() } +//! # fn view(state: &State) -> iced::Element<'_, Message> { iced::widget::text("").into() } //! ``` //! //! A [`Subscription`] is [a _declarative_ builder of streams](Subscription#the-lifetime-of-a-subscription) @@ -452,7 +452,7 @@ //! } //! } //! -//! fn view(state: &State) -> Element { +//! fn view(state: &State) -> Element<'_, Message> { //! match &state.screen { //! Screen::Contacts(contacts) => contacts.view().map(Message::Contacts), //! Screen::Conversation(conversation) => conversation.view().map(Message::Conversation), diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index f6360211..55fe34e4 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -339,8 +339,8 @@ impl Span { /// /// fn view(&self) -> Element<'_, Message> { /// markdown::view(&self.markdown, Theme::TokyoNight) -/// .map(Message::LinkClicked) -/// .into() +/// .map(Message::LinkClicked) +/// .into() /// } /// /// fn update(state: &mut State, message: Message) { From 10bbe44c3024fbd1b0b7b274234788632c1bfa3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 6 Jun 2025 22:58:59 +0200 Subject: [PATCH 14/25] Draft experimental hotpatching support :tada: Thanks to `subsecond` by the Dioxus folks! --- Cargo.lock | 85 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + debug/Cargo.toml | 5 ++- debug/src/lib.rs | 32 ++++++++++++++- runtime/src/lib.rs | 5 +++ src/application.rs | 22 +++++++---- src/application/timed.rs | 25 ++++++++---- src/daemon.rs | 24 ++++++++---- winit/src/lib.rs | 40 +++++++++++++++++-- winit/src/proxy.rs | 8 ++-- 10 files changed, 212 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b90d4cec..10abae26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,6 +519,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -702,6 +722,17 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cargo-hot-protocol" +version = "0.1.0" +source = "git+https://github.com/hecrj/cargo-hot.git?rev=e71ddcd1d37be79ddb6f22262600798319235ba5#e71ddcd1d37be79ddb6f22262600798319235ba5" +dependencies = [ + "anyhow", + "bincode 2.0.1", + "log", + "subsecond", +] + [[package]] name = "cast" version = "0.3.0" @@ -2419,7 +2450,7 @@ dependencies = [ name = "iced_beacon" version = "0.14.0-dev" dependencies = [ - "bincode", + "bincode 1.3.3", "futures", "iced_core", "log", @@ -2451,6 +2482,7 @@ dependencies = [ name = "iced_debug" version = "0.14.0-dev" dependencies = [ + "cargo-hot-protocol", "iced_beacon", "iced_core", "iced_futures", @@ -3244,6 +3276,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.44", +] + [[package]] name = "memmap2" version = "0.9.5" @@ -5406,6 +5447,34 @@ dependencies = [ "rayon", ] +[[package]] +name = "subsecond" +version = "0.7.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5b40acd555d02d9a0b5bf4080dbf2cd085d5e2eb2ae7851cb14b9bf5af15c" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.12", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bedadae58a56e137ac970c38c44bff38cee24400fef64c37d5a188a065b1ec1f" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5483,7 +5552,7 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" dependencies = [ - "bincode", + "bincode 1.3.3", "bitflags 1.3.2", "flate2", "fnv", @@ -6169,6 +6238,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" @@ -6288,6 +6363,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "visible_bounds" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3c19290b..daa5c8e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,6 +167,7 @@ bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" +cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "e71ddcd1d37be79ddb6f22262600798319235ba5" } cosmic-text = "0.14" dark-light = "2.0" futures = { version = "0.3", default-features = false } diff --git a/debug/Cargo.toml b/debug/Cargo.toml index f6c7c843..a9ffcf38 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -11,7 +11,7 @@ categories.workspace = true keywords.workspace = true [features] -enable = ["dep:iced_beacon"] +enable = ["dep:iced_beacon", "dep:cargo-hot"] [dependencies] iced_core.workspace = true @@ -21,3 +21,6 @@ log.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_beacon.workspace = true iced_beacon.optional = true + +cargo-hot.workspace = true +cargo-hot.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 17463f9a..7d64cb60 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -113,6 +113,14 @@ pub fn commands() -> Subscription { internal::commands() } +pub fn hot(f: impl FnOnce() -> O) -> O { + internal::hot(f) +} + +pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { + internal::on_hotpatch(f) +} + #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] mod internal { use crate::core::theme; @@ -129,7 +137,7 @@ mod internal { use beacon::span::present; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; - use std::sync::{LazyLock, RwLock}; + use std::sync::{Arc, LazyLock, RwLock}; pub fn init(metadata: Metadata) { let name = metadata.name.split("::").next().unwrap_or(metadata.name); @@ -140,6 +148,8 @@ mod internal { theme: metadata.theme, can_time_travel: metadata.can_time_travel, }; + + cargo_hot::connect(); } pub fn quit() -> bool { @@ -271,6 +281,20 @@ mod internal { Subscription::run(listen_for_commands) } + pub fn hot(f: impl FnOnce() -> O) -> O { + let mut f = Some(f); + + // The `move` here is important. Hotpatching will not work + // otherwise. + cargo_hot::subsecond::call(move || { + f.take().expect("Hot function is stale")() + }) + } + + pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { + cargo_hot::subsecond::register_handler(Arc::new(f)); + } + fn span(span: span::Stage) -> Span { log(client::Event::SpanStarted(span.clone())); @@ -398,4 +422,10 @@ mod internal { impl Span { pub fn finish(self) {} } + + pub fn hot(f: impl FnOnce() -> O) -> O { + f() + } + + pub fn on_hotpatch(_f: impl Fn() + Send + Sync + 'static) {} } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 47bd92d9..457f723c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -56,6 +56,9 @@ pub enum Action { /// Run a system action. System(system::Action), + /// Recreate all user interfaces and redraw all windows. + Reload, + /// Exits the runtime. /// /// This will normally close any application windows and @@ -79,6 +82,7 @@ impl Action { Action::Clipboard(action) => Err(Action::Clipboard(action)), Action::Window(action) => Err(Action::Window(action)), Action::System(action) => Err(Action::System(action)), + Action::Reload => Err(Action::Reload), Action::Exit => Err(Action::Exit), } } @@ -102,6 +106,7 @@ where } Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Reload => write!(f, "Action::Reload"), Action::Exit => write!(f, "Action::Exit"), } } diff --git a/src/application.rs b/src/application.rs index e87d89a2..e735218b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -38,6 +38,8 @@ use crate::{ Element, Executor, Font, Result, Settings, Size, Subscription, Task, }; +use iced_debug as debug; + use std::borrow::Cow; pub mod timed; @@ -126,7 +128,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - self.update.update(state, message) + debug::hot(|| self.update.update(state, message)) } fn view<'a>( @@ -134,7 +136,7 @@ where state: &'a Self::State, _window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state) + debug::hot(|| self.view.view(state)) } } @@ -327,7 +329,7 @@ impl Application

{ > { Application { raw: program::with_title(self.raw, move |state, _window| { - title.title(state) + debug::hot(|| title.title(state)) }), settings: self.settings, window: self.window, @@ -342,7 +344,9 @@ impl Application

{ impl Program, > { Application { - raw: program::with_subscription(self.raw, f), + raw: program::with_subscription(self.raw, move |state| { + debug::hot(|| f(state)) + }), settings: self.settings, window: self.window, } @@ -356,7 +360,9 @@ impl Application

{ impl Program, > { Application { - raw: program::with_theme(self.raw, move |state, _window| f(state)), + raw: program::with_theme(self.raw, move |state, _window| { + debug::hot(|| f(state)) + }), settings: self.settings, window: self.window, } @@ -370,7 +376,9 @@ impl Application

{ impl Program, > { Application { - raw: program::with_style(self.raw, f), + raw: program::with_style(self.raw, move |state, theme| { + debug::hot(|| f(state, theme)) + }), settings: self.settings, window: self.window, } @@ -385,7 +393,7 @@ impl Application

{ > { Application { raw: program::with_scale_factor(self.raw, move |state, _window| { - f(state) + debug::hot(|| f(state)) }), settings: self.settings, window: self.window, diff --git a/src/application/timed.rs b/src/application/timed.rs index 606273c8..3d158874 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -6,6 +6,8 @@ use crate::time::Instant; use crate::window; use crate::{Element, Program, Settings, Subscription, Task}; +use iced_debug as debug; + /// Creates an [`Application`] with an `update` function that also /// takes the [`Instant`] of each `Message`. /// @@ -97,10 +99,12 @@ where state: &mut Self::State, (message, now): Self::Message, ) -> Task { - self.update - .update(state, message, now) - .into() - .map(|message| (message, Instant::now())) + debug::hot(move || { + self.update + .update(state, message, now) + .into() + .map(|message| (message, Instant::now())) + }) } fn view<'a>( @@ -108,16 +112,21 @@ where state: &'a Self::State, _window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view - .view(state) - .map(|message| (message, Instant::now())) + debug::hot(|| { + self.view + .view(state) + .map(|message| (message, Instant::now())) + }) } fn subscription( &self, state: &Self::State, ) -> self::Subscription { - (self.subscription)(state).map(|message| (message, Instant::now())) + debug::hot(|| { + (self.subscription)(state) + .map(|message| (message, Instant::now())) + }) } } diff --git a/src/daemon.rs b/src/daemon.rs index 1b99da30..2074cf71 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -6,6 +6,8 @@ use crate::theme; use crate::window; use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; +use iced_debug as debug; + use std::borrow::Cow; /// Creates an iced [`Daemon`] given its boot, update, and view logic. @@ -72,7 +74,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - self.update.update(state, message) + debug::hot(|| self.update.update(state, message)) } fn view<'a>( @@ -80,7 +82,7 @@ where state: &'a Self::State, window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state, window) + debug::hot(|| self.view.view(state, window)) } } @@ -176,7 +178,7 @@ impl Daemon

{ > { Daemon { raw: program::with_title(self.raw, move |state, window| { - title.title(state, window) + debug::hot(|| title.title(state, window)) }), settings: self.settings, } @@ -190,7 +192,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_subscription(self.raw, f), + raw: program::with_subscription(self.raw, move |state| { + debug::hot(|| f(state)) + }), settings: self.settings, } } @@ -203,7 +207,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_theme(self.raw, f), + raw: program::with_theme(self.raw, move |state, window| { + debug::hot(|| f(state, window)) + }), settings: self.settings, } } @@ -216,7 +222,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_style(self.raw, f), + raw: program::with_style(self.raw, move |state, theme| { + debug::hot(|| f(state, theme)) + }), settings: self.settings, } } @@ -229,7 +237,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_scale_factor(self.raw, f), + raw: program::with_scale_factor(self.raw, move |state, window| { + debug::hot(|| f(state, window)) + }), settings: self.settings, } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 7b94a878..5f1dfb22 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -85,6 +85,15 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); + #[cfg(feature = "debug")] + { + let proxy = proxy.clone(); + + debug::on_hotpatch(move || { + proxy.send_action(Action::Reload); + }); + } + let mut runtime = { let executor = P::Executor::new().map_err(Error::ExecutorCreationFailed)?; @@ -527,7 +536,7 @@ async fn run_instance

( let create_compositor = { let window = window.clone(); - let mut proxy = proxy.clone(); + let proxy = proxy.clone(); let default_fonts = default_fonts.clone(); async move { @@ -1075,9 +1084,9 @@ fn update( runtime.track(recipes); } -fn run_action( +fn run_action<'a, P, C>( action: Action, - program: &program::Instance

, + program: &'a program::Instance

, compositor: &mut Option, events: &mut Vec<(window::Id, core::Event)>, messages: &mut Vec, @@ -1085,7 +1094,7 @@ fn run_action( control_sender: &mut mpsc::UnboundedSender, interfaces: &mut FxHashMap< window::Id, - UserInterface<'_, P::Message, P::Theme, P::Renderer>, + UserInterface<'a, P::Message, P::Theme, P::Renderer>, >, window_manager: &mut WindowManager, ui_caches: &mut FxHashMap, @@ -1437,6 +1446,29 @@ fn run_action( let _ = channel.send(Ok(())); } } + Action::Reload => { + for (id, window) in window_manager.iter_mut() { + let Some(ui) = interfaces.remove(&id) else { + continue; + }; + + let cache = ui.into_cache(); + let size = window.size(); + + let _ = interfaces.insert( + id, + build_user_interface( + program, + cache, + &mut window.renderer, + size, + id, + ), + ); + + window.raw.request_redraw(); + } + } Action::Exit => { control_sender .start_send(Control::Exit) diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index d8d3f4a2..dcfcacff 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -77,7 +77,7 @@ impl Proxy { /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send(&mut self, value: T) + pub fn send(&self, value: T) where T: std::fmt::Debug, { @@ -88,13 +88,11 @@ impl Proxy { /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send_action(&mut self, action: Action) + pub fn send_action(&self, action: Action) where T: std::fmt::Debug, { - self.raw - .send_event(action) - .expect("Send message to event loop"); + let _ = self.raw.send_event(action); } /// Frees an amount of slots for additional messages to be queued in From c1c5fbda613fd58a1da149321aff34a6b482da71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 7 Jun 2025 04:50:52 +0200 Subject: [PATCH 15/25] Update `cargo-hot` --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10abae26..3f517be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "cargo-hot-protocol" version = "0.1.0" -source = "git+https://github.com/hecrj/cargo-hot.git?rev=e71ddcd1d37be79ddb6f22262600798319235ba5#e71ddcd1d37be79ddb6f22262600798319235ba5" +source = "git+https://github.com/hecrj/cargo-hot.git?rev=b8dc518b8640928178a501257e353b73bc06cf47#b8dc518b8640928178a501257e353b73bc06cf47" dependencies = [ "anyhow", "bincode 2.0.1", diff --git a/Cargo.toml b/Cargo.toml index daa5c8e0..bd7caad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,7 +167,7 @@ bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" -cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "e71ddcd1d37be79ddb6f22262600798319235ba5" } +cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "b8dc518b8640928178a501257e353b73bc06cf47" } cosmic-text = "0.14" dark-light = "2.0" futures = { version = "0.3", default-features = false } From 699b85762ba3cf3757c23ed9aa698d8ba3711e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Jun 2025 02:32:22 +0200 Subject: [PATCH 16/25] Try to detect stale type changes when hotpatching --- debug/src/lib.rs | 55 ++++++++++++++++++++++++++++++++++++++++++--- devtools/src/lib.rs | 31 ++++++++++++++++--------- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 7d64cb60..99def674 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -121,6 +121,10 @@ pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { internal::on_hotpatch(f) } +pub fn is_stale() -> bool { + internal::is_stale() +} + #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] mod internal { use crate::core::theme; @@ -136,8 +140,16 @@ mod internal { use beacon::span; use beacon::span::present; + use std::collections::BTreeSet; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; - use std::sync::{Arc, LazyLock, RwLock}; + use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock}; + + static IS_STALE: AtomicBool = AtomicBool::new(false); + + static HOT_FUNCTIONS_PENDING: Mutex> = + Mutex::new(BTreeSet::new()); + + static HOT_FUNCTIONS: OnceLock> = OnceLock::new(); pub fn init(metadata: Metadata) { let name = metadata.name.split("::").next().unwrap_or(metadata.name); @@ -150,6 +162,20 @@ mod internal { }; cargo_hot::connect(); + + cargo_hot::subsecond::register_handler(Arc::new(|| { + if HOT_FUNCTIONS.get().is_none() { + HOT_FUNCTIONS + .set(std::mem::take( + &mut HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions"), + )) + .expect("Set hot functions"); + } + + IS_STALE.store(false, atomic::Ordering::Relaxed); + })); } pub fn quit() -> bool { @@ -286,15 +312,34 @@ mod internal { // The `move` here is important. Hotpatching will not work // otherwise. - cargo_hot::subsecond::call(move || { + let mut f = cargo_hot::subsecond::HotFn::current(move || { f.take().expect("Hot function is stale")() - }) + }); + + let address = f.ptr_address().0; + + if let Some(hot_functions) = HOT_FUNCTIONS.get() { + if hot_functions.contains(&address) { + IS_STALE.store(true, atomic::Ordering::Relaxed); + } + } else { + let _ = HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions") + .insert(address); + } + + f.call(()) } pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { cargo_hot::subsecond::register_handler(Arc::new(f)); } + pub fn is_stale() -> bool { + IS_STALE.load(atomic::Ordering::Relaxed) + } + fn span(span: span::Stage) -> Span { log(client::Event::SpanStarted(span.clone())); @@ -428,4 +473,8 @@ mod internal { } pub fn on_hotpatch(_f: impl Fn() + Send + Sync + 'static) {} + + pub fn is_stale() -> bool { + false + } } diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index e4c2e4d4..45409267 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -343,21 +343,30 @@ where themer(derive_theme(), Element::from(mode).map(Event::Message)) }); - let notification = self.show_notification.then(|| { - themer( - derive_theme(), - bottom_right(opaque( - container(text("Press F12 to open debug metrics")) - .padding(10) - .style(container::dark), - )), - ) - }); + let notification = self + .show_notification + .then(|| text("Press F12 to open debug metrics")) + .or_else(|| { + debug::is_stale().then(|| { + text( + "Types have changed. Restart to re-enable hotpatching.", + ) + }) + }); stack![view] .height(Fill) .push_maybe(mode.map(opaque)) - .push_maybe(notification) + .push_maybe(notification.map(|notification| { + themer( + derive_theme(), + bottom_right(opaque( + container(notification) + .padding(10) + .style(container::dark), + )), + ) + })) .into() } From e689ec13a6a7b2fd0401b9f1f7152e262c903964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Jun 2025 17:37:08 +0200 Subject: [PATCH 17/25] Place hot reloading behind `hot` feature flag --- Cargo.toml | 4 +- debug/Cargo.toml | 3 +- debug/src/lib.rs | 138 ++++++++++++++++++++++++++--------------------- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd7caad1..c9a3eea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,12 @@ qr_code = ["iced_widget/qr_code"] markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] -# Enables a debug view in native platforms (press F12) +# Enables debug metrics in native platforms (press F12) debug = ["iced_winit/debug", "iced_devtools"] # Enables time-travel debugging (very experimental!) time-travel = ["debug", "iced_devtools/time-travel"] +# Enables hot reloading (very experimental!) +hot = ["debug", "iced_debug/hot"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms diff --git a/debug/Cargo.toml b/debug/Cargo.toml index a9ffcf38..1c0abda3 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -11,7 +11,8 @@ categories.workspace = true keywords.workspace = true [features] -enable = ["dep:iced_beacon", "dep:cargo-hot"] +enable = ["dep:iced_beacon"] +hot = ["enable", "dep:cargo-hot"] [dependencies] iced_core.workspace = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 99def674..f79dde55 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -39,6 +39,7 @@ pub fn disable() { pub fn init(metadata: Metadata) { internal::init(metadata); + hot::init(); } pub fn quit() -> bool { @@ -114,15 +115,15 @@ pub fn commands() -> Subscription { } pub fn hot(f: impl FnOnce() -> O) -> O { - internal::hot(f) + hot::call(f) } pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { - internal::on_hotpatch(f) + hot::on_hotpatch(f) } pub fn is_stale() -> bool { - internal::is_stale() + hot::is_stale() } #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] @@ -140,16 +141,8 @@ mod internal { use beacon::span; use beacon::span::present; - use std::collections::BTreeSet; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; - use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock}; - - static IS_STALE: AtomicBool = AtomicBool::new(false); - - static HOT_FUNCTIONS_PENDING: Mutex> = - Mutex::new(BTreeSet::new()); - - static HOT_FUNCTIONS: OnceLock> = OnceLock::new(); + use std::sync::{LazyLock, RwLock}; pub fn init(metadata: Metadata) { let name = metadata.name.split("::").next().unwrap_or(metadata.name); @@ -160,22 +153,6 @@ mod internal { theme: metadata.theme, can_time_travel: metadata.can_time_travel, }; - - cargo_hot::connect(); - - cargo_hot::subsecond::register_handler(Arc::new(|| { - if HOT_FUNCTIONS.get().is_none() { - HOT_FUNCTIONS - .set(std::mem::take( - &mut HOT_FUNCTIONS_PENDING - .lock() - .expect("Lock hot functions"), - )) - .expect("Set hot functions"); - } - - IS_STALE.store(false, atomic::Ordering::Relaxed); - })); } pub fn quit() -> bool { @@ -307,39 +284,6 @@ mod internal { Subscription::run(listen_for_commands) } - pub fn hot(f: impl FnOnce() -> O) -> O { - let mut f = Some(f); - - // The `move` here is important. Hotpatching will not work - // otherwise. - let mut f = cargo_hot::subsecond::HotFn::current(move || { - f.take().expect("Hot function is stale")() - }); - - let address = f.ptr_address().0; - - if let Some(hot_functions) = HOT_FUNCTIONS.get() { - if hot_functions.contains(&address) { - IS_STALE.store(true, atomic::Ordering::Relaxed); - } - } else { - let _ = HOT_FUNCTIONS_PENDING - .lock() - .expect("Lock hot functions") - .insert(address); - } - - f.call(()) - } - - pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { - cargo_hot::subsecond::register_handler(Arc::new(f)); - } - - pub fn is_stale() -> bool { - IS_STALE.load(atomic::Ordering::Relaxed) - } - fn span(span: span::Stage) -> Span { log(client::Event::SpanStarted(span.clone())); @@ -467,8 +411,78 @@ mod internal { impl Span { pub fn finish(self) {} } +} - pub fn hot(f: impl FnOnce() -> O) -> O { +#[cfg(feature = "hot")] +mod hot { + use std::collections::BTreeSet; + use std::sync::atomic::{self, AtomicBool}; + use std::sync::{Arc, Mutex, OnceLock}; + + static IS_STALE: AtomicBool = AtomicBool::new(false); + + static HOT_FUNCTIONS_PENDING: Mutex> = + Mutex::new(BTreeSet::new()); + + static HOT_FUNCTIONS: OnceLock> = OnceLock::new(); + + pub fn init() { + cargo_hot::connect(); + + cargo_hot::subsecond::register_handler(Arc::new(|| { + if HOT_FUNCTIONS.get().is_none() { + HOT_FUNCTIONS + .set(std::mem::take( + &mut HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions"), + )) + .expect("Set hot functions"); + } + + IS_STALE.store(false, atomic::Ordering::Relaxed); + })); + } + + pub fn call(f: impl FnOnce() -> O) -> O { + let mut f = Some(f); + + // The `move` here is important. Hotpatching will not work + // otherwise. + let mut f = cargo_hot::subsecond::HotFn::current(move || { + f.take().expect("Hot function is stale")() + }); + + let address = f.ptr_address().0; + + if let Some(hot_functions) = HOT_FUNCTIONS.get() { + if hot_functions.contains(&address) { + IS_STALE.store(true, atomic::Ordering::Relaxed); + } + } else { + let _ = HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions") + .insert(address); + } + + f.call(()) + } + + pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { + cargo_hot::subsecond::register_handler(Arc::new(f)); + } + + pub fn is_stale() -> bool { + IS_STALE.load(atomic::Ordering::Relaxed) + } +} + +#[cfg(not(feature = "hot"))] +mod hot { + pub fn init() {} + + pub fn call(f: impl FnOnce() -> O) -> O { f() } From 8578a1362bc4b02fa28a2bf9ad1df1898e3629b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 00:00:49 +0200 Subject: [PATCH 18/25] Fix `clippy` lints for Rust 1.88 :tada: --- examples/clock/src/main.rs | 2 +- examples/multi_window/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- examples/toast/src/main.rs | 11 +++++------ examples/tour/src/main.rs | 2 +- wgpu/src/window/compositor.rs | 2 +- widget/src/pane_grid.rs | 10 +++++----- winit/src/lib.rs | 2 +- 8 files changed, 16 insertions(+), 17 deletions(-) diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 428b1e63..a1050a76 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -166,7 +166,7 @@ impl canvas::Program for Clock { let y = radius * angle.0.sin(); frame.fill_text(canvas::Text { - content: format!("{}", hour), + content: format!("{hour}"), size: (radius / 5.0).into(), position: Point::new(x * 0.82, y * 0.82), color: palette.secondary.strong.text, diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index ff20c468..77be505b 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -161,7 +161,7 @@ impl Example { impl Window { fn new(count: usize) -> Self { Self { - title: format!("Window_{}", count), + title: format!("Window_{count}"), scale_input: "1.0".to_string(), current_scale: 1.0, theme: Theme::ALL[count % Theme::ALL.len()].clone(), diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index e4d0fc68..1f18e285 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -174,7 +174,7 @@ impl Example { |png_result| match png_result { Ok(path) => format!("Png saved as: {path:?}!"), Err(PngError(error)) => { - format!("Png could not be saved due to:\n{}", error) + format!("Png could not be saved due to:\n{error}") } }, ); diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index f1a8cfa5..b4e93a32 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -613,12 +613,11 @@ mod toast { &self.viewport, renderer, ) - .max( - cursor - .is_over(layout.bounds()) - .then_some(mouse::Interaction::Idle) - .unwrap_or_default(), - ) + .max(if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + Default::default() + }) }) .max() .unwrap_or_default() diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index a08c96f3..984cf272 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -76,7 +76,7 @@ impl Tour { Screen::End => "End", }; - format!("{} - Iced", screen) + format!("{screen} - Iced") } fn update(&mut self, event: Message) { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 60cb33bb..c729ddd0 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -96,7 +96,7 @@ impl Compositor { let adapter = instance .request_adapter(&adapter_options) .await - .ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?; + .ok_or(Error::NoAdapterFound(format!("{adapter_options:?}")))?; log::info!("Selected: {:#?}", adapter.get_info()); diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 53206792..db60fabc 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -297,11 +297,11 @@ where } fn drag_enabled(&self) -> bool { - self.internal - .maximized() - .is_none() - .then(|| self.on_drag.is_some()) - .unwrap_or_default() + if self.internal.maximized().is_none() { + self.on_drag.is_some() + } else { + Default::default() + } } fn grid_interaction( diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 5f1dfb22..c4e62863 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -810,7 +810,7 @@ async fn run_instance

( Err(error) => match error { // This is an unrecoverable error. compositor::SurfaceError::OutOfMemory => { - panic!("{:?}", error); + panic!("{error:?}"); } _ => { present_span.finish(); From 482d3482309eed649d07159e0fc08e4fdfd81b55 Mon Sep 17 00:00:00 2001 From: Brock Szuszczewicz Date: Thu, 26 Jun 2025 14:48:56 -0600 Subject: [PATCH 19/25] Use `Limits::min` directly in `scrollable::layout` --- widget/src/scrollable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index ff366db0..01c4ef7c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -438,7 +438,7 @@ where }, |limits| { let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), + limits.min(), Size::new( if self.direction.horizontal().is_some() { f32::INFINITY From 5ad08acd97a6fb6ed1e94f547d7b431d6f89f9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 16:47:05 +0200 Subject: [PATCH 20/25] Defer `flex` layout of elements with fixed length in the main axis --- core/src/layout/flex.rs | 53 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 2cff5bfd..f178a993 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -137,16 +137,24 @@ where // // We use the maximum cross length obtained in the first pass as the maximum // cross limit. + // + // We can defer the layout of any elements that have a fixed size in the main axis, + // allowing them to use the cross calculations of the next pass. if cross_compress && some_fill_cross { for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let (fill_main_factor, fill_cross_factor) = { + let (main_size, cross_size) = { let size = child.as_widget().size(); - axis.pack(size.width.fill_factor(), size.height.fill_factor()) + axis.pack(size.width, size.height) }; - if fill_main_factor == 0 && fill_cross_factor != 0 { + if main_size.fill_factor() == 0 && cross_size.fill_factor() != 0 { + if let Length::Fixed(main) = main_size { + available -= main; + continue; + } + let (max_width, max_height) = axis.pack(available, cross); let child_limits = @@ -176,9 +184,9 @@ where }; // THIRD PASS - // We only have the elements that are fluid in the main axis left. + // We lay out the elements that are fluid in the main axis. // We use the remaining space to evenly allocate space based on fill factors. - for (i, (child, tree)) in items.iter().zip(trees).enumerate() { + 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(); @@ -224,10 +232,43 @@ where } } + // FOURTH PASS (conditional) + // We lay out any elements that were deferred in the second pass. + // These are elements that must be compressed in their cross axis and have + // a fixed length in the main axis. + if cross_compress && some_fill_cross { + for (i, (child, tree)) in items.iter().zip(trees).enumerate() { + let (main_size, cross_size) = { + let size = child.as_widget().size(); + + axis.pack(size.width, size.height) + }; + + if cross_size.fill_factor() != 0 { + let Length::Fixed(main) = main_size else { + continue; + }; + + let (max_width, max_height) = axis.pack(main, 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(); + + cross = cross.max(axis.cross(size)); + + nodes[i] = layout; + } + } + } + let pad = axis.pack(padding.left, padding.top); let mut main = pad.0; - // FOURTH PASS + // FIFTH PASS // We align all the laid out nodes in the cross axis, if needed. for (i, node) in nodes.iter_mut().enumerate() { if i > 0 { From ca1bf717b33549c02784a27589b3fc694243d0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 16:47:54 +0200 Subject: [PATCH 21/25] Implement `Quote` support in `markdown` widget --- widget/src/container.rs | 4 +- widget/src/markdown.rs | 89 +++++++++++++++++++++++++++++++++++++++-- widget/src/rule.rs | 15 ++----- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index 4f6725b1..84bb5237 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -400,9 +400,9 @@ where Renderer: core::Renderer + 'a, { fn from( - column: Container<'a, Message, Theme, Renderer>, + container: Container<'a, Message, Theme, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { - Element::new(column) + Element::new(container) } } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 55fe34e4..a7d012a1 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -50,7 +50,10 @@ use crate::core::theme; use crate::core::{ self, Color, Element, Length, Padding, Pixels, Theme, color, }; -use crate::{column, container, rich_text, row, scrollable, span, text}; +use crate::{ + column, container, rich_text, row, rule, scrollable, span, text, + vertical_rule, +}; use std::borrow::BorrowMut; use std::cell::{Cell, RefCell}; @@ -208,6 +211,8 @@ pub enum Item { /// The alternative text of the image. alt: Text, }, + /// A quote. + Quote(Vec), } /// A bunch of parsed Markdown text. @@ -454,6 +459,7 @@ fn parse_with<'a>( ) -> impl Iterator)> + 'a { enum Scope { List(List), + Quote(Vec), } struct List { @@ -524,6 +530,9 @@ fn parse_with<'a>( Scope::List(list) => { list.items.last_mut().expect("item context").push(item); } + Scope::Quote(items) => { + items.push(item); + } } None @@ -605,6 +614,22 @@ fn parse_with<'a>( None } + pulldown_cmark::Tag::BlockQuote(_kind) if !metadata && !table => { + let prev = if spans.is_empty() { + None + } else { + produce( + state.borrow_mut(), + &mut stack, + Item::Paragraph(Text::new(spans.drain(..).collect())), + source, + ) + }; + + stack.push(Scope::Quote(Vec::new())); + + prev + } pulldown_cmark::Tag::CodeBlock( pulldown_cmark::CodeBlockKind::Fenced(language), ) if !metadata && !table => { @@ -703,7 +728,9 @@ fn parse_with<'a>( pulldown_cmark::TagEnd::List(_) if !metadata && !table => { let scope = stack.pop()?; - let Scope::List(list) = scope; + let Scope::List(list) = scope else { + return None; + }; produce( state.borrow_mut(), @@ -715,6 +742,22 @@ fn parse_with<'a>( source, ) } + pulldown_cmark::TagEnd::BlockQuote(_kind) + if !metadata && !table => + { + let scope = stack.pop()?; + + let Scope::Quote(quote) = scope else { + return None; + }; + + produce( + state.borrow_mut(), + &mut stack, + Item::Quote(quote), + source, + ) + } pulldown_cmark::TagEnd::Image if !metadata && !table => { let (url, title) = image.take()?; let alt = Text::new(spans.drain(..).collect()); @@ -1063,6 +1106,7 @@ where start: Some(start), items, } => viewer.ordered_list(settings, *start, items), + Item::Quote(quote) => viewer.quote(settings, quote), } } @@ -1226,7 +1270,33 @@ where .into() } -/// A view strategy to display a Markdown [`Item`].j +/// Displays a quote using the default look. +pub fn quote<'a, Message, Theme, Renderer>( + viewer: &impl Viewer<'a, Message, Theme, Renderer>, + settings: Settings, + contents: &'a [Item], +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + row![ + vertical_rule(4), + column( + contents + .iter() + .enumerate() + .map(|(i, content)| item(viewer, settings, content, i)), + ) + .spacing(settings.spacing.0), + ] + .height(Length::Shrink) + .spacing(settings.spacing.0) + .into() +} + +/// A view strategy to display a Markdown [`Item`]. pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Self: Sized + 'a, @@ -1321,6 +1391,17 @@ where ) -> Element<'a, Message, Theme, Renderer> { ordered_list(self, settings, start, items) } + + /// Displays a quote. + /// + /// By default, it call [`quote`]. + fn quote( + &self, + settings: Settings, + contents: &'a [Item], + ) -> Element<'a, Message, Theme, Renderer> { + quote(self, settings, contents) + } } #[derive(Debug, Clone, Copy)] @@ -1338,7 +1419,7 @@ where /// The theme catalog of Markdown items. pub trait Catalog: - container::Catalog + scrollable::Catalog + text::Catalog + container::Catalog + scrollable::Catalog + rule::Catalog + text::Catalog { /// The styling class of a Markdown code block. fn code_block<'a>() -> ::Class<'a>; diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 03c57c94..2bb893b2 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -134,9 +134,7 @@ where let style = theme.style(&self.class); let bounds = if self.is_horizontal { - let line_y = (bounds.y + (bounds.height / 2.0) - - (style.width as f32 / 2.0)) - .round(); + let line_y = (bounds.y + (bounds.height / 2.0)).round(); let (offset, line_width) = style.fill_mode.fill(bounds.width); let line_x = bounds.x + offset; @@ -145,12 +143,10 @@ where x: line_x, y: line_y, width: line_width, - height: style.width as f32, + height: bounds.height, } } else { - let line_x = (bounds.x + (bounds.width / 2.0) - - (style.width as f32 / 2.0)) - .round(); + let line_x = (bounds.x + (bounds.width / 2.0)).round(); let (offset, line_height) = style.fill_mode.fill(bounds.height); let line_y = bounds.y + offset; @@ -158,7 +154,7 @@ where Rectangle { x: line_x, y: line_y, - width: style.width as f32, + width: bounds.width, height: line_height, } }; @@ -192,8 +188,6 @@ where pub struct Style { /// The color of the rule. pub color: Color, - /// The width (thickness) of the rule line. - pub width: u16, /// The radius of the line corners. pub radius: border::Radius, /// The [`FillMode`] of the rule. @@ -301,7 +295,6 @@ pub fn default(theme: &Theme) -> Style { Style { color: palette.background.strong.color, - width: 1, radius: 0.0.into(), fill_mode: FillMode::Full, snap: true, From 0ff616814d537ac43dea502e10ccc5c4324fe047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 16:51:00 +0200 Subject: [PATCH 22/25] Fix typo in docs of `markdown::Viewer::quote` --- widget/src/markdown.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index a7d012a1..3fc18bc3 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -1394,7 +1394,7 @@ where /// Displays a quote. /// - /// By default, it call [`quote`]. + /// By default, it calls [`quote`]. fn quote( &self, settings: Settings, From ef13eb58b2582ba4f87f250522bdc70945eb2f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 17:05:38 +0200 Subject: [PATCH 23/25] Fix `styling` example and regenerate snapshots --- .../styling/snapshots/catppuccin_frappé-tiny-skia.sha256 | 2 +- .../styling/snapshots/catppuccin_latte-tiny-skia.sha256 | 2 +- .../styling/snapshots/catppuccin_macchiato-tiny-skia.sha256 | 2 +- .../styling/snapshots/catppuccin_mocha-tiny-skia.sha256 | 2 +- examples/styling/snapshots/dark-tiny-skia.sha256 | 2 +- examples/styling/snapshots/dracula-tiny-skia.sha256 | 2 +- examples/styling/snapshots/ferra-tiny-skia.sha256 | 2 +- examples/styling/snapshots/gruvbox_dark-tiny-skia.sha256 | 2 +- examples/styling/snapshots/gruvbox_light-tiny-skia.sha256 | 2 +- examples/styling/snapshots/kanagawa_dragon-tiny-skia.sha256 | 2 +- examples/styling/snapshots/kanagawa_lotus-tiny-skia.sha256 | 2 +- examples/styling/snapshots/kanagawa_wave-tiny-skia.sha256 | 2 +- examples/styling/snapshots/light-tiny-skia.sha256 | 2 +- examples/styling/snapshots/moonfly-tiny-skia.sha256 | 2 +- examples/styling/snapshots/nightfly-tiny-skia.sha256 | 2 +- examples/styling/snapshots/nord-tiny-skia.sha256 | 2 +- examples/styling/snapshots/oxocarbon-tiny-skia.sha256 | 2 +- examples/styling/snapshots/solarized_dark-tiny-skia.sha256 | 2 +- examples/styling/snapshots/solarized_light-tiny-skia.sha256 | 2 +- examples/styling/snapshots/tokyo_night-tiny-skia.sha256 | 2 +- .../styling/snapshots/tokyo_night_light-tiny-skia.sha256 | 2 +- .../styling/snapshots/tokyo_night_storm-tiny-skia.sha256 | 2 +- examples/styling/src/main.rs | 6 +++--- 23 files changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 b/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 index 0749a625..3309ffc7 100644 --- a/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 +++ b/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 @@ -1 +1 @@ -12ba47a34ed415825a23f8ef377a2d52950d2f8614a66bf46c0ec28d0cf15c85 \ No newline at end of file +30570747bb062e9f7730cdd58be961c84bcf4711a6983185bff6d903e8d29e9c \ No newline at end of file diff --git a/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 b/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 index 1993b7c9..8c303beb 100644 --- a/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 +++ b/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 @@ -1 +1 @@ -fa00d7e0ff95b366945d409712d7fe4ce44fff22425236cb56b8b96a88815ee6 \ No newline at end of file +039a950d09d34df222606eaf2166e65f86d5536f8abfaff0e295219c974d54c0 \ No newline at end of file diff --git a/examples/styling/snapshots/catppuccin_macchiato-tiny-skia.sha256 b/examples/styling/snapshots/catppuccin_macchiato-tiny-skia.sha256 index 08924463..ec379562 100644 --- a/examples/styling/snapshots/catppuccin_macchiato-tiny-skia.sha256 +++ b/examples/styling/snapshots/catppuccin_macchiato-tiny-skia.sha256 @@ -1 +1 @@ -4e594cfec775d51f7f836646c59bf4a2de07252721d66ddddea69c17e9112bae \ No newline at end of file +30e523961db89a3ee97ad1eac09e727ecb3dec485faa362534a9f5ad083b32dd \ No newline at end of file diff --git a/examples/styling/snapshots/catppuccin_mocha-tiny-skia.sha256 b/examples/styling/snapshots/catppuccin_mocha-tiny-skia.sha256 index 9c7466a8..86583521 100644 --- a/examples/styling/snapshots/catppuccin_mocha-tiny-skia.sha256 +++ b/examples/styling/snapshots/catppuccin_mocha-tiny-skia.sha256 @@ -1 +1 @@ -2ab665b51387c61086ae0199c29e291105bfe4583bd4c4daa652e30917f10bd6 \ No newline at end of file +bce5427d5105f68e1d7fa18a34fcc551cb78c2fefd9a583ba44686331133436d \ No newline at end of file diff --git a/examples/styling/snapshots/dark-tiny-skia.sha256 b/examples/styling/snapshots/dark-tiny-skia.sha256 index 6fc0c8ec..5453059b 100644 --- a/examples/styling/snapshots/dark-tiny-skia.sha256 +++ b/examples/styling/snapshots/dark-tiny-skia.sha256 @@ -1 +1 @@ -61c9ee377b33ffa800f512877e45ad5f41fbac36f5d3f06d1b62d6af6ee9d7b2 \ No newline at end of file +c8a7edbd5a8bbf559134b84253e14e65340f4ffe3e22c272b21c8438e47ffaf7 \ No newline at end of file diff --git a/examples/styling/snapshots/dracula-tiny-skia.sha256 b/examples/styling/snapshots/dracula-tiny-skia.sha256 index 3be82338..d4b41911 100644 --- a/examples/styling/snapshots/dracula-tiny-skia.sha256 +++ b/examples/styling/snapshots/dracula-tiny-skia.sha256 @@ -1 +1 @@ -75f2fb12c9090a256708515de01a25e78905f71e134b7cda79f4fe44b2434052 \ No newline at end of file +63d646b22d3dffbb56dac2e3f345090bd26625a388dd6cc142359f2a7ac9c8df \ No newline at end of file diff --git a/examples/styling/snapshots/ferra-tiny-skia.sha256 b/examples/styling/snapshots/ferra-tiny-skia.sha256 index 04a5e3c8..2e2c2eab 100644 --- a/examples/styling/snapshots/ferra-tiny-skia.sha256 +++ b/examples/styling/snapshots/ferra-tiny-skia.sha256 @@ -1 +1 @@ -b4a1b42d2e21b2a493605745e6beb8e1f28cbeb01b73336e1e8d9061249a8311 \ No newline at end of file +d26f55674cbd96bc3b534ffdd098a13199718ef9c5ffe8ece0882ddab714b776 \ No newline at end of file diff --git a/examples/styling/snapshots/gruvbox_dark-tiny-skia.sha256 b/examples/styling/snapshots/gruvbox_dark-tiny-skia.sha256 index afb98574..8249f9c3 100644 --- a/examples/styling/snapshots/gruvbox_dark-tiny-skia.sha256 +++ b/examples/styling/snapshots/gruvbox_dark-tiny-skia.sha256 @@ -1 +1 @@ -eb52921b3ee23e1814268701c935d0dff387e7eb741c50443f75a7ab902b5e44 \ No newline at end of file +482c44c13d4ff3de19e71f3dddf93bbee170e54e2d353e818811069de28e18ed \ No newline at end of file diff --git a/examples/styling/snapshots/gruvbox_light-tiny-skia.sha256 b/examples/styling/snapshots/gruvbox_light-tiny-skia.sha256 index 0f7d8d13..0277b685 100644 --- a/examples/styling/snapshots/gruvbox_light-tiny-skia.sha256 +++ b/examples/styling/snapshots/gruvbox_light-tiny-skia.sha256 @@ -1 +1 @@ -bf6c4cbd6eeed0167d28509e37292f5ce26ed1d58bb156bedb861d0619a1945b \ No newline at end of file +6738cc4fc6eb8a5d406c613a4b0f08c0e8dcd2c1a5444445eebd3888f9303841 \ No newline at end of file diff --git a/examples/styling/snapshots/kanagawa_dragon-tiny-skia.sha256 b/examples/styling/snapshots/kanagawa_dragon-tiny-skia.sha256 index 0df00ff1..7995a848 100644 --- a/examples/styling/snapshots/kanagawa_dragon-tiny-skia.sha256 +++ b/examples/styling/snapshots/kanagawa_dragon-tiny-skia.sha256 @@ -1 +1 @@ -83726a4175900a9ef159b80925f2fa985b4ea87bff78d8bb01918fb6c40d6175 \ No newline at end of file +0a918c52538fc4848aa0c68d8f2d6f4c981ed68971dd9c725f0093a39ef7f353 \ No newline at end of file diff --git a/examples/styling/snapshots/kanagawa_lotus-tiny-skia.sha256 b/examples/styling/snapshots/kanagawa_lotus-tiny-skia.sha256 index aaf8d822..b68211a1 100644 --- a/examples/styling/snapshots/kanagawa_lotus-tiny-skia.sha256 +++ b/examples/styling/snapshots/kanagawa_lotus-tiny-skia.sha256 @@ -1 +1 @@ -54c8d44afbdd644f324cf40e744b3d7871263e44de2d9a91f6c10470c38ac3c6 \ No newline at end of file +de3e1a2c21e1a86d76ca99989c73e8a2596ef627bba95d246fab8f02d56bd0af \ No newline at end of file diff --git a/examples/styling/snapshots/kanagawa_wave-tiny-skia.sha256 b/examples/styling/snapshots/kanagawa_wave-tiny-skia.sha256 index 0d9d3f5b..d9848230 100644 --- a/examples/styling/snapshots/kanagawa_wave-tiny-skia.sha256 +++ b/examples/styling/snapshots/kanagawa_wave-tiny-skia.sha256 @@ -1 +1 @@ -5713843396e2efcfc7cc8abd00343d5d66ce8b8a195212a9b75dbfeec8edf7a7 \ No newline at end of file +3418ea4eb0f7786607ef02e7db4bc97309530f2f7c08f8aea15c768a13a09ca4 \ No newline at end of file diff --git a/examples/styling/snapshots/light-tiny-skia.sha256 b/examples/styling/snapshots/light-tiny-skia.sha256 index 73d274c7..e9864b81 100644 --- a/examples/styling/snapshots/light-tiny-skia.sha256 +++ b/examples/styling/snapshots/light-tiny-skia.sha256 @@ -1 +1 @@ -f1b20ab79f8242776d9eb1ad9cff7090435aa416811c48a7c22c69b09cd8e70f \ No newline at end of file +c8474e02a9df23f123816a489c1ea7ae6cb994a0eca429592dfe6d933de1beee \ No newline at end of file diff --git a/examples/styling/snapshots/moonfly-tiny-skia.sha256 b/examples/styling/snapshots/moonfly-tiny-skia.sha256 index 2d8f2b3c..6456da10 100644 --- a/examples/styling/snapshots/moonfly-tiny-skia.sha256 +++ b/examples/styling/snapshots/moonfly-tiny-skia.sha256 @@ -1 +1 @@ -2c4be9dc1340b65cad6d15d5318017412eba1247a016379b83db379dde0214af \ No newline at end of file +02095fd09c078be02dc41e29e55de25e8a79e6ad4293aa7e430257a9016dfb3d \ No newline at end of file diff --git a/examples/styling/snapshots/nightfly-tiny-skia.sha256 b/examples/styling/snapshots/nightfly-tiny-skia.sha256 index c174fbe6..f2ef8296 100644 --- a/examples/styling/snapshots/nightfly-tiny-skia.sha256 +++ b/examples/styling/snapshots/nightfly-tiny-skia.sha256 @@ -1 +1 @@ -2d5d6b9613ccb6a2f23142baf704288037808e7a60ee05bdc73490d8c8780064 \ No newline at end of file +d82588a2aba3e7211f25b85ebb812a42dfa59137dd4b59d26f5f60d5b28e537f \ No newline at end of file diff --git a/examples/styling/snapshots/nord-tiny-skia.sha256 b/examples/styling/snapshots/nord-tiny-skia.sha256 index 57bd936a..3905db54 100644 --- a/examples/styling/snapshots/nord-tiny-skia.sha256 +++ b/examples/styling/snapshots/nord-tiny-skia.sha256 @@ -1 +1 @@ -512b8dfd4687a609d202436e75ecf1a5553acc2157000e77e31c1578941b033c \ No newline at end of file +d6b73545929cc7794c1a918f069b5326ef129bed8f9ad2cd001be7d078a2b6a0 \ No newline at end of file diff --git a/examples/styling/snapshots/oxocarbon-tiny-skia.sha256 b/examples/styling/snapshots/oxocarbon-tiny-skia.sha256 index c36f7223..363fcf31 100644 --- a/examples/styling/snapshots/oxocarbon-tiny-skia.sha256 +++ b/examples/styling/snapshots/oxocarbon-tiny-skia.sha256 @@ -1 +1 @@ -4b3b7a2dc65307a3551227f1c5d2bb49da15d29e320ccaa160e3d9fca44c1037 \ No newline at end of file +0ec7251c69755becd678b7aec398a275edf31cc077960723cd6b9364e8678548 \ No newline at end of file diff --git a/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 b/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 index 0b341569..6de6c786 100644 --- a/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 +++ b/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 @@ -1 +1 @@ -15aa476c65304bde23e3648ceb424d6394f525f6c1d3e49ee1150376b6fd3068 \ No newline at end of file +d87f3a00de5ee52db12a21c09e8689d8d489cce6eb8c8ce1b89192b85587e9a0 \ No newline at end of file diff --git a/examples/styling/snapshots/solarized_light-tiny-skia.sha256 b/examples/styling/snapshots/solarized_light-tiny-skia.sha256 index b62d6107..163561bc 100644 --- a/examples/styling/snapshots/solarized_light-tiny-skia.sha256 +++ b/examples/styling/snapshots/solarized_light-tiny-skia.sha256 @@ -1 +1 @@ -04fcb0da640c6e7c1d86c95c74570c9f9d8f56056c9802eef332187f52c003ca \ No newline at end of file +7be707e7b8fb91e82c3cc33bf6c12e5c1a37db65616a35f5999351c26bbd0e50 \ No newline at end of file diff --git a/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 b/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 index b4afd06f..2607513b 100644 --- a/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 +++ b/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 @@ -1 +1 @@ -26e9660da3caa88e5a995739c986b142487f1449c1a5dc555abf7bc7cbca2345 \ No newline at end of file +3c0b41e13decdc826ddf05e3805c33f624bccc1f1fbe39b2fb8e6880ad18e472 \ No newline at end of file diff --git a/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 b/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 index 29ba7bca..77b3136d 100644 --- a/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 +++ b/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 @@ -1 +1 @@ -8d846a765ff96506ad4f26a672976cb1bfa997d4b09c5ecabf273d5e22ae0a3a \ No newline at end of file +1bb6f75eb390cae16dab87947623e15f0e7dafdb79e89f700d0fbf16129ffd1b \ No newline at end of file diff --git a/examples/styling/snapshots/tokyo_night_storm-tiny-skia.sha256 b/examples/styling/snapshots/tokyo_night_storm-tiny-skia.sha256 index 3bf421ff..ac5f0ad2 100644 --- a/examples/styling/snapshots/tokyo_night_storm-tiny-skia.sha256 +++ b/examples/styling/snapshots/tokyo_night_storm-tiny-skia.sha256 @@ -1 +1 @@ -0ecd51994f6eb37f111dc1b21cad72bb705499eb83156e9dc3ae2221ec392a42 \ No newline at end of file +533d25575e8bf1111036fb082b424d0d0e60947a7da8428ab8c71e0bda01469e \ No newline at end of file diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index b78e04b9..934b9f5d 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -126,7 +126,7 @@ impl Styling { let content = column![ choose_theme, - horizontal_rule(38), + horizontal_rule(1), text_input, row![primary, success, warning, danger] .spacing(10) @@ -135,8 +135,8 @@ impl Styling { progress_bar(), row![ scrollable, - vertical_rule(38), - column![checkbox, toggler].spacing(20) + row![vertical_rule(1), column![checkbox, toggler].spacing(20)] + .spacing(20) ] .spacing(10) .height(100) From 946e2a73b780182f7002452fd2645d10b3f24064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 18:46:25 +0200 Subject: [PATCH 24/25] Implement `Rule` support in `markdown` widget --- widget/src/markdown.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 3fc18bc3..be88c5f9 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -51,8 +51,8 @@ use crate::core::{ self, Color, Element, Length, Padding, Pixels, Theme, color, }; use crate::{ - column, container, rich_text, row, rule, scrollable, span, text, - vertical_rule, + column, container, horizontal_rule, rich_text, row, rule, scrollable, span, + text, vertical_rule, }; use std::borrow::BorrowMut; @@ -213,6 +213,8 @@ pub enum Item { }, /// A quote. Quote(Vec), + /// A horizontal separator. + Rule, } /// A bunch of parsed Markdown text. @@ -877,6 +879,9 @@ fn parse_with<'a>( }); None } + pulldown_cmark::Event::Rule => { + produce(state.borrow_mut(), &mut stack, Item::Rule, source) + } _ => None, }) } @@ -1107,6 +1112,7 @@ where items, } => viewer.ordered_list(settings, *start, items), Item::Quote(quote) => viewer.quote(settings, quote), + Item::Rule => viewer.rule(settings), } } @@ -1296,6 +1302,17 @@ where .into() } +/// Displays a rule using the default look. +pub fn rule<'a, Message, Theme, Renderer>() +-> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + horizontal_rule(2).into() +} + /// A view strategy to display a Markdown [`Item`]. pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where @@ -1402,6 +1419,16 @@ where ) -> Element<'a, Message, Theme, Renderer> { quote(self, settings, contents) } + + /// Displays a rule. + /// + /// By default, it calls [`rule`](self::rule()). + fn rule( + &self, + _settings: Settings, + ) -> Element<'a, Message, Theme, Renderer> { + rule() + } } #[derive(Debug, Clone, Copy)] From 283d0e74a8050ea625da25e0b9180b65f11d1843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 29 Jun 2025 14:42:59 +0200 Subject: [PATCH 25/25] Annotate `Shell` methods with `#[must_use]` --- core/src/shell.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/shell.rs b/core/src/shell.rs index e3fcdf89..31659bfb 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -32,6 +32,7 @@ impl<'a, Message> Shell<'a, Message> { } /// Returns true if the [`Shell`] contains no published messages + #[must_use] pub fn is_empty(&self) -> bool { self.messages.is_empty() } @@ -50,11 +51,13 @@ impl<'a, Message> Shell<'a, Message> { } /// Returns the current [`event::Status`] of the [`Shell`]. + #[must_use] pub fn event_status(&self) -> event::Status { self.event_status } /// Returns whether the current event has been captured. + #[must_use] pub fn is_event_captured(&self) -> bool { self.event_status == event::Status::Captured } @@ -73,6 +76,7 @@ impl<'a, Message> Shell<'a, Message> { } /// Returns the request a redraw should happen, if any. + #[must_use] pub fn redraw_request(&self) -> window::RedrawRequest { self.redraw_request } @@ -101,16 +105,19 @@ impl<'a, Message> Shell<'a, Message> { } /// Returns the current [`InputMethod`] strategy. + #[must_use] pub fn input_method(&self) -> &InputMethod { &self.input_method } /// Returns the current [`InputMethod`] strategy. + #[must_use] pub fn input_method_mut(&mut self) -> &mut InputMethod { &mut self.input_method } /// Returns whether the current layout is invalid or not. + #[must_use] pub fn is_layout_invalid(&self) -> bool { self.is_layout_invalid } @@ -134,6 +141,7 @@ impl<'a, Message> Shell<'a, Message> { /// Returns whether the widgets of the current application have been /// invalidated. + #[must_use] pub fn are_widgets_invalid(&self) -> bool { self.are_widgets_invalid }