From 15f1566578b32baca5facd439af6c3d0e152320c Mon Sep 17 00:00:00 2001 From: gigas002 Date: Fri, 15 Mar 2024 21:54:49 +0900 Subject: [PATCH 001/657] Implement content_fit for viewer --- widget/src/image/viewer.rs | 126 +++++++++++++++---------------------- 1 file changed, 49 insertions(+), 77 deletions(-) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 2e3fd713..fc578911 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,4 +1,5 @@ //! Zoom and pan on an image. +use iced_renderer::core::ContentFit; use crate::core::event::{self, Event}; use crate::core::image; use crate::core::layout; @@ -23,6 +24,7 @@ pub struct Viewer { scale_step: f32, handle: Handle, filter_method: image::FilterMethod, + content_fit: ContentFit, } impl Viewer { @@ -37,6 +39,7 @@ impl Viewer { max_scale: 10.0, scale_step: 0.10, filter_method: image::FilterMethod::default(), + content_fit: ContentFit::Contain, } } @@ -46,6 +49,12 @@ impl Viewer { self } + /// Sets the [`iced_renderer::core::ContentFit`] of the [`Viewer`]. + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } + /// Sets the padding of the [`Viewer`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into().0; @@ -117,36 +126,25 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let Size { width, height } = renderer.dimensions(&self.handle); + let image_size = { + let Size { width, height } = renderer.dimensions(&self.handle); + Size::new(width as f32, height as f32) + }; + let raw_size = limits.resolve(self.width, self.height, image_size); + let full_size = self.content_fit.fit(image_size, raw_size); - let mut size = limits.resolve( - self.width, - self.height, - Size::new(width as f32, height as f32), - ); - - let expansion_size = if height > width { - self.width - } else { - self.height + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, }; - // Only calculate viewport sizes if the images are constrained to a limited space. - // If they are Fill|Portion let them expand within their allotted space. - match expansion_size { - Length::Shrink | Length::Fixed(_) => { - let aspect_ratio = width as f32 / height as f32; - let viewport_aspect_ratio = size.width / size.height; - if viewport_aspect_ratio > aspect_ratio { - size.width = width as f32 * size.height / height as f32; - } else { - size.height = height as f32 * size.width / width as f32; - } - } - Length::Fill | Length::FillPortion(_) => {} - } - - layout::Node::new(size) + layout::Node::new(final_size) } fn on_event( @@ -184,12 +182,7 @@ where }) .clamp(self.min_scale, self.max_scale); - let image_size = image_size( - renderer, - &self.handle, - state, - bounds.size(), - ); + let image_size = image_size(renderer, &self.handle, state, bounds.size(), self.content_fit); let factor = state.scale / previous_scale - 1.0; @@ -231,7 +224,7 @@ where } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { let state = tree.state.downcast_mut::(); - + if state.cursor_grabbed_at.is_some() { state.cursor_grabbed_at = None; @@ -242,15 +235,9 @@ where } Event::Mouse(mouse::Event::CursorMoved { position }) => { let state = tree.state.downcast_mut::(); - + if let Some(origin) = state.cursor_grabbed_at { - let image_size = image_size( - renderer, - &self.handle, - state, - bounds.size(), - ); - + let image_size = image_size(renderer, &self.handle, state, bounds.size(), self.content_fit); let hidden_width = (image_size.width - bounds.width / 2.0) .max(0.0) .round(); @@ -321,32 +308,30 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let image_size = - image_size(renderer, &self.handle, state, bounds.size()); + let image_size = image_size(renderer, &self.handle, state, bounds.size(), self.content_fit); - let translation = { + let translation = { let image_top_left = Vector::new( - bounds.width / 2.0 - image_size.width / 2.0, - bounds.height / 2.0 - image_size.height / 2.0, + (bounds.width - image_size.width).max(0.0) / 2.0, + (bounds.height - image_size.height).max(0.0) / 2.0, ); image_top_left - state.offset(bounds, image_size) }; - renderer.with_layer(bounds, |renderer| { + let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { - image::Renderer::draw( - renderer, - self.handle.clone(), - self.filter_method, - Rectangle { - x: bounds.x, - y: bounds.y, - ..Rectangle::with_size(image_size) - }, - ); + let drawing_bounds = Rectangle { + width: image_size.width, + height: image_size.height, + ..bounds + }; + + renderer.draw(self.handle.clone(), self.filter_method, drawing_bounds); }); - }); + }; + + renderer.with_layer(bounds, render); } } @@ -417,27 +402,14 @@ pub fn image_size( handle: &::Handle, state: &State, bounds: Size, + content_fit: ContentFit, ) -> Size where Renderer: image::Renderer, { - let Size { width, height } = renderer.dimensions(handle); + let size = renderer.dimensions(handle); + let size = Size::new(size.width as f32, size.height as f32); + let size = content_fit.fit(size, bounds); - let (width, height) = { - let dimensions = (width as f32, height as f32); - - let width_ratio = bounds.width / dimensions.0; - let height_ratio = bounds.height / dimensions.1; - - let ratio = width_ratio.min(height_ratio); - let scale = state.scale; - - if ratio < 1.0 { - (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) - } else { - (dimensions.0 * scale, dimensions.1 * scale) - } - }; - - Size::new(width, height) + Size::new(size.width * state.scale, size.height * state.scale) } From c9453cd55d84f0dd2ad0050208863d036c98843f Mon Sep 17 00:00:00 2001 From: gigas002 Date: Fri, 15 Mar 2024 22:06:15 +0900 Subject: [PATCH 002/657] run cargo fmt --- widget/src/image/viewer.rs | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index fc578911..bd10e953 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,5 +1,4 @@ //! Zoom and pan on an image. -use iced_renderer::core::ContentFit; use crate::core::event::{self, Event}; use crate::core::image; use crate::core::layout; @@ -10,6 +9,7 @@ use crate::core::{ Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; +use iced_renderer::core::ContentFit; use std::hash::Hash; @@ -182,7 +182,13 @@ where }) .clamp(self.min_scale, self.max_scale); - let image_size = image_size(renderer, &self.handle, state, bounds.size(), self.content_fit); + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + self.content_fit, + ); let factor = state.scale / previous_scale - 1.0; @@ -224,7 +230,7 @@ where } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { let state = tree.state.downcast_mut::(); - + if state.cursor_grabbed_at.is_some() { state.cursor_grabbed_at = None; @@ -235,9 +241,15 @@ where } Event::Mouse(mouse::Event::CursorMoved { position }) => { let state = tree.state.downcast_mut::(); - + if let Some(origin) = state.cursor_grabbed_at { - let image_size = image_size(renderer, &self.handle, state, bounds.size(), self.content_fit); + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + self.content_fit, + ); let hidden_width = (image_size.width - bounds.width / 2.0) .max(0.0) .round(); @@ -308,9 +320,15 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let image_size = image_size(renderer, &self.handle, state, bounds.size(), self.content_fit); + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + self.content_fit, + ); - let translation = { + let translation = { let image_top_left = Vector::new( (bounds.width - image_size.width).max(0.0) / 2.0, (bounds.height - image_size.height).max(0.0) / 2.0, @@ -327,7 +345,11 @@ where ..bounds }; - renderer.draw(self.handle.clone(), self.filter_method, drawing_bounds); + renderer.draw( + self.handle.clone(), + self.filter_method, + drawing_bounds, + ); }); }; From 6146382676a7bff4764e86e99d0d053f5fbbc045 Mon Sep 17 00:00:00 2001 From: Richard Custodio Date: Mon, 18 Mar 2024 16:48:15 -0300 Subject: [PATCH 003/657] feat: add `text` macro to `widget::helpers` --- widget/src/helpers.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 4863e550..1b57cd21 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -51,6 +51,19 @@ macro_rules! row { ); } +/// Creates a new [`Text`] widget with the provided content. +/// +/// [`Text`]: core::widget::Text +#[macro_export] +macro_rules! text { + () => ( + $crate::Text::new() + ); + ($($arg:tt)*) => { + $crate::Text::new(format!($($arg)*)) + }; +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container From bf9bbf4a3edf22f21c79901999cc104cb29fccce Mon Sep 17 00:00:00 2001 From: Richard Custodio Date: Mon, 18 Mar 2024 17:08:56 -0300 Subject: [PATCH 004/657] refactor: replace `text(format!(` with `text` macro --- examples/custom_quad/src/main.rs | 4 +-- examples/custom_widget/src/main.rs | 2 +- examples/download_progress/src/main.rs | 2 +- examples/events/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 2 +- examples/integration/src/controls.rs | 2 +- examples/lazy/src/main.rs | 2 +- examples/loading_spinners/src/main.rs | 2 +- examples/pane_grid/src/main.rs | 2 +- examples/pokedex/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 2 +- examples/stopwatch/src/main.rs | 4 +-- examples/system_information/src/main.rs | 36 ++++++++++++------------ examples/toast/src/main.rs | 2 +- examples/todos/src/main.rs | 4 +-- examples/tour/src/main.rs | 8 +++--- examples/vectorial_text/src/main.rs | 2 +- 18 files changed, 41 insertions(+), 41 deletions(-) diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index c093e240..d8aac1d0 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -165,7 +165,7 @@ impl Example { self.border_width, self.shadow ), - text(format!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}")), + text!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}"), slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01), slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01), slider(1.0..=100.0, br, Message::RadiusBottomRightChanged) @@ -174,7 +174,7 @@ impl Example { .step(0.01), slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) .step(0.01), - text(format!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}")), + text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"), slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged) .step(0.01), slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged) diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index aa49ebd0..0c9e774d 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -114,7 +114,7 @@ impl Example { fn view(&self) -> Element { let content = column![ circle(self.radius), - text(format!("Radius: {:.2}", self.radius)), + text!("Radius: {:.2}", self.radius), slider(1.0..=100.0, self.radius, Message::RadiusChanged).step(0.01), ] .padding(20) diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 9f4769e0..a4136415 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -166,7 +166,7 @@ impl Download { .into() } State::Downloading { .. } => { - text(format!("Downloading... {current_progress:.2}%")).into() + text!("Downloading... {current_progress:.2}%").into() } State::Errored => column![ "Something went wrong :(", diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index bf568c94..4734e20c 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -61,7 +61,7 @@ impl Events { let events = Column::with_children( self.last .iter() - .map(|event| text(format!("{event:?}")).size(40)) + .map(|event| text!("{event:?}").size(40)) .map(Element::from), ); diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 2b0fae0b..48574247 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -163,7 +163,7 @@ fn view_controls<'a>( let speed_controls = row![ slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), - text(format!("x{speed}")).size(16), + text!("x{speed}").size(16), ] .align_items(Alignment::Center) .spacing(10); diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 28050f8a..5359d54f 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -78,7 +78,7 @@ impl Program for Controls { container( column![ text("Background color").color(Color::WHITE), - text(format!("{background_color:?}")) + text!("{background_color:?}") .size(14) .color(Color::WHITE), text_input("Placeholder", &self.input) diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 2d53df93..627aba23 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -192,7 +192,7 @@ impl App { text_input("Add a new option", &self.input) .on_input(Message::InputChanged) .on_submit(Message::AddItem(self.input.clone())), - button(text(format!("Toggle Order ({})", self.order))) + button(text!("Toggle Order ({})", self.order)) .on_press(Message::ToggleOrder) ] .spacing(10) diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index eaa4d57e..2b2abad5 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -81,7 +81,7 @@ impl LoadingSpinners { Message::CycleDurationChanged(x / 100.0) }) .width(200.0), - text(format!("{:.2}s", self.cycle_duration)), + text!("{:.2}s", self.cycle_duration), ] .align_items(iced::Alignment::Center) .spacing(20.0), diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 829996d8..d4981024 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -285,7 +285,7 @@ fn view_content<'a>( .max_width(160); let content = column![ - text(format!("{}x{}", size.width, size.height)).size(24), + text!("{}x{}", size.width, size.height).size(24), controls, ] .spacing(10) diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 0811c08d..61e75116 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -109,7 +109,7 @@ impl Pokemon { column![ row![ text(&self.name).size(30).width(Length::Fill), - text(format!("#{}", self.number)) + text!("#{}", self.number) .size(20) .color([0.5, 0.5, 0.5]), ] diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index d887c41b..c73d8dfd 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -163,7 +163,7 @@ impl Example { .push_maybe( self.crop_error .as_ref() - .map(|error| text(format!("Crop error! \n{error}"))), + .map(|error| text!("Crop error! \n{error}")), ) .spacing(10) .align_items(Alignment::Center); diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 07ae05d6..b805e7d5 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -54,7 +54,7 @@ impl SierpinskiEmulator { .width(Length::Fill) .height(Length::Fill), row![ - text(format!("Iteration: {:?}", self.graph.iteration)), + text!("Iteration: {:?}", self.graph.iteration), slider(0..=10000, self.graph.iteration, Message::IterationSet) ] .padding(10) diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index b9eb19cf..6bd5ce3e 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -92,13 +92,13 @@ impl Stopwatch { let seconds = self.duration.as_secs(); - let duration = text(format!( + let duration = text!( "{:0>2}:{:0>2}:{:0>2}.{:0>2}", seconds / HOUR, (seconds % HOUR) / MINUTE, seconds % MINUTE, self.duration.subsec_millis() / 10, - )) + ) .size(40); let button = |label| { diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index a6ac27a6..cd4d9cb3 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -45,56 +45,56 @@ impl Example { let content: Element<_> = match self { Example::Loading => text("Loading...").size(40).into(), Example::Loaded { information } => { - let system_name = text(format!( + let system_name = text!( "System name: {}", information .system_name .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); - let system_kernel = text(format!( + let system_kernel = text!( "System kernel: {}", information .system_kernel .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); - let system_version = text(format!( + let system_version = text!( "System version: {}", information .system_version .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); - let system_short_version = text(format!( + let system_short_version = text!( "System short version: {}", information .system_short_version .as_ref() .unwrap_or(&"unknown".to_string()) - )); + ); let cpu_brand = - text(format!("Processor brand: {}", information.cpu_brand)); + text!("Processor brand: {}", information.cpu_brand); - let cpu_cores = text(format!( + let cpu_cores = text!( "Processor cores: {}", information .cpu_cores .map_or("unknown".to_string(), |cores| cores .to_string()) - )); + ); let memory_readable = ByteSize::b(information.memory_total).to_string(); - let memory_total = text(format!( + let memory_total = text!( "Memory (total): {} bytes ({memory_readable})", information.memory_total, - )); + ); let memory_text = if let Some(memory_used) = information.memory_used @@ -106,17 +106,17 @@ impl Example { String::from("None") }; - let memory_used = text(format!("Memory (used): {memory_text}")); + let memory_used = text!("Memory (used): {memory_text}"); - let graphics_adapter = text(format!( + let graphics_adapter = text!( "Graphics adapter: {}", information.graphics_adapter - )); + ); - let graphics_backend = text(format!( + let graphics_backend = text!( "Graphics backend: {}", information.graphics_backend - )); + ); column![ system_name.size(30), diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index fdae1dc1..4916ceb6 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -131,7 +131,7 @@ impl App { subtitle( "Timeout", row![ - text(format!("{:0>2} sec", self.timeout_secs)), + text!("{:0>2} sec", self.timeout_secs), slider( 1.0..=30.0, self.timeout_secs as f64, diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 7768c1d5..f5fb94c9 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -396,10 +396,10 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { }; row![ - text(format!( + text!( "{tasks_left} {} left", if tasks_left == 1 { "task" } else { "tasks" } - )) + ) .width(Length::Fill), row![ filter_button("All", Filter::All, current_filter), diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index a88c0dba..3fb8b460 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -433,7 +433,7 @@ impl<'a> Step { let spacing_section = column![ slider(0..=80, spacing, StepMessage::SpacingChanged), - text(format!("{spacing} px")) + text!("{spacing} px") .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ] @@ -457,7 +457,7 @@ impl<'a> Step { fn text(size: u16, color: Color) -> Column<'a, StepMessage> { let size_section = column![ "You can change its size:", - text(format!("This text is {size} pixels")).size(size), + text!("This text is {size} pixels").size(size), slider(10..=70, size, StepMessage::TextSizeChanged), ] .padding(20) @@ -472,7 +472,7 @@ impl<'a> Step { let color_section = column![ "And its color:", - text(format!("{color:?}")).color(color), + text!("{color:?}").color(color), color_sliders, ] .padding(20) @@ -544,7 +544,7 @@ impl<'a> Step { .push(ferris(width, filter_method)) .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) .push( - text(format!("Width: {width} px")) + text!("Width: {width} px") .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index a7391e23..91740a37 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -55,7 +55,7 @@ impl VectorialText { row![ text(label), horizontal_space(), - text(format!("{:.2}", value)) + text!("{:.2}", value) ], slider(range, value, message).step(0.01) ] From db7d8680ce198439921c8856b2d6d0ccfa4d66ff Mon Sep 17 00:00:00 2001 From: Richard Custodio Date: Mon, 18 Mar 2024 17:46:22 -0300 Subject: [PATCH 005/657] docs: improve `text` macro documentation --- widget/src/helpers.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1b57cd21..966d23cc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -54,6 +54,23 @@ macro_rules! row { /// Creates a new [`Text`] widget with the provided content. /// /// [`Text`]: core::widget::Text +/// +/// This macro uses the same syntax as [`format!`], but creates a new [`Text`] widget instead. +/// +/// See [the formatting documentation in `std::fmt`](std::fmt) +/// for details of the macro argument syntax. +/// +/// # Examples +/// +/// ``` +/// fn view(&self) -> Element { +/// let empty = text!(); +/// let simple = text!("Hello, world!"); +/// let keyword = text!("Hello, {}", "world!"); +/// let planet = "Earth"; +/// let complex = text!("Hello, {planet}!"); +/// } +/// ``` #[macro_export] macro_rules! text { () => ( From 8ed62541af8cd16760b59d8f0a49d619d78f592e Mon Sep 17 00:00:00 2001 From: Richard Custodio Date: Mon, 18 Mar 2024 18:24:57 -0300 Subject: [PATCH 006/657] fix: run `cargo fmt` --- examples/integration/src/controls.rs | 4 +--- examples/pane_grid/src/main.rs | 10 ++++------ examples/pokedex/src/main.rs | 4 +--- examples/system_information/src/main.rs | 12 ++++-------- examples/vectorial_text/src/main.rs | 6 +----- 5 files changed, 11 insertions(+), 25 deletions(-) diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 5359d54f..1958b2f3 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -78,9 +78,7 @@ impl Program for Controls { container( column![ text("Background color").color(Color::WHITE), - text!("{background_color:?}") - .size(14) - .color(Color::WHITE), + text!("{background_color:?}").size(14).color(Color::WHITE), text_input("Placeholder", &self.input) .on_input(Message::InputChanged), sliders, diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index d4981024..9e78ad0b 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -284,12 +284,10 @@ fn view_content<'a>( .spacing(5) .max_width(160); - let content = column![ - text!("{}x{}", size.width, size.height).size(24), - controls, - ] - .spacing(10) - .align_items(Alignment::Center); + let content = + column![text!("{}x{}", size.width, size.height).size(24), controls,] + .spacing(10) + .align_items(Alignment::Center); container(scrollable(content)) .width(Length::Fill) diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 61e75116..6ba6fe66 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -109,9 +109,7 @@ impl Pokemon { column![ row![ text(&self.name).size(30).width(Length::Fill), - text!("#{}", self.number) - .size(20) - .color([0.5, 0.5, 0.5]), + text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]), ] .align_items(Alignment::Center) .spacing(20), diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index cd4d9cb3..cae764dc 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -108,15 +108,11 @@ impl Example { let memory_used = text!("Memory (used): {memory_text}"); - let graphics_adapter = text!( - "Graphics adapter: {}", - information.graphics_adapter - ); + let graphics_adapter = + text!("Graphics adapter: {}", information.graphics_adapter); - let graphics_backend = text!( - "Graphics backend: {}", - information.graphics_backend - ); + let graphics_backend = + text!("Graphics backend: {}", information.graphics_backend); column![ system_name.size(30), diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 91740a37..1ed7a2b1 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -52,11 +52,7 @@ impl VectorialText { fn view(&self) -> Element { let slider_with_label = |label, range, value, message: fn(f32) -> _| { column![ - row![ - text(label), - horizontal_space(), - text!("{:.2}", value) - ], + row![text(label), horizontal_space(), text!("{:.2}", value)], slider(range, value, message).step(0.01) ] .spacing(2) From d71e78d1384c885be1ceba6e1f5c871174ca9c74 Mon Sep 17 00:00:00 2001 From: Richard Custodio Date: Mon, 18 Mar 2024 18:30:29 -0300 Subject: [PATCH 007/657] fix: remove empty macro usage --- widget/src/helpers.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 966d23cc..5eea7cc2 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -64,18 +64,17 @@ macro_rules! row { /// /// ``` /// fn view(&self) -> Element { -/// let empty = text!(); /// let simple = text!("Hello, world!"); +/// /// let keyword = text!("Hello, {}", "world!"); +/// /// let planet = "Earth"; -/// let complex = text!("Hello, {planet}!"); +/// let local_variable = text!("Hello, {planet}!"); +/// // ... /// } /// ``` #[macro_export] macro_rules! text { - () => ( - $crate::Text::new() - ); ($($arg:tt)*) => { $crate::Text::new(format!($($arg)*)) }; From 72ed8bcc8def9956e25f3720a3095fc96bb2eef0 Mon Sep 17 00:00:00 2001 From: Richard Custodio Date: Mon, 18 Mar 2024 20:24:42 -0300 Subject: [PATCH 008/657] fix: make `text` macro example pass doctest --- widget/src/helpers.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 5eea7cc2..b294a1d4 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -62,15 +62,32 @@ macro_rules! row { /// /// # Examples /// -/// ``` -/// fn view(&self) -> Element { -/// let simple = text!("Hello, world!"); +/// ```no_run +/// # mod iced { +/// # pub struct Element(pub std::marker::PhantomData); +/// # pub mod widget { +/// # macro_rules! text { +/// # ($($arg:tt)*) => {unimplemented!()} +/// # } +/// # pub(crate) use text; +/// # } +/// # } +/// # struct Example; +/// # enum Message {} +/// use iced::Element; +/// use iced::widget::text; /// -/// let keyword = text!("Hello, {}", "world!"); +/// impl Example { +/// fn view(&self) -> Element { +/// let simple = text!("Hello, world!"); /// -/// let planet = "Earth"; -/// let local_variable = text!("Hello, {planet}!"); -/// // ... +/// let keyword = text!("Hello, {}", "world!"); +/// +/// let planet = "Earth"; +/// let local_variable = text!("Hello, {planet}!"); +/// // ... +/// # iced::Element(std::marker::PhantomData) +/// } /// } /// ``` #[macro_export] From 13bd106fc585034a7aba17b9c17589113274aaf5 Mon Sep 17 00:00:00 2001 From: gigas002 Date: Fri, 29 Mar 2024 20:55:21 +0900 Subject: [PATCH 009/657] Minor renaming refactoring --- widget/src/image/viewer.rs | 70 +++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index fba00028..e57857c1 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,10 +6,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Rectangle, + Shell, Size, Vector, Widget, }; -use iced_renderer::core::ContentFit; use std::hash::Hash; @@ -49,7 +48,7 @@ impl Viewer { self } - /// Sets the [`iced_renderer::core::ContentFit`] of the [`Viewer`]. + /// Sets the [`ContentFit`] of the [`Viewer`]. pub fn content_fit(mut self, content_fit: ContentFit) -> Self { self.content_fit = content_fit; self @@ -126,20 +125,26 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { + // The raw w/h of the underlying image let image_size = { let Size { width, height } = renderer.measure_image(&self.handle); Size::new(width as f32, height as f32) }; - let raw_size = limits.resolve(self.width, self.height, image_size); - let full_size = self.content_fit.fit(image_size, raw_size); + // The size to be available to the widget prior to `Shrink`ing + let raw_size = limits.resolve(self.width, self.height, image_size); + + // The uncropped size of the image when fit to the bounds above + let fit_size = self.content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested let final_size = Size { width: match self.width { - Length::Shrink => f32::min(raw_size.width, full_size.width), + Length::Shrink => f32::min(raw_size.width, fit_size.width), _ => raw_size.width, }, height: match self.height { - Length::Shrink => f32::min(raw_size.height, full_size.height), + Length::Shrink => f32::min(raw_size.height, fit_size.height), _ => raw_size.height, }, }; @@ -182,7 +187,7 @@ where }) .clamp(self.min_scale, self.max_scale); - let image_size = image_size( + let scaled_size = scaled_image_size( renderer, &self.handle, state, @@ -199,12 +204,12 @@ where + state.current_offset * factor; state.current_offset = Vector::new( - if image_size.width > bounds.width { + if scaled_size.width > bounds.width { state.current_offset.x + adjustment.x } else { 0.0 }, - if image_size.height > bounds.height { + if scaled_size.height > bounds.height { state.current_offset.y + adjustment.y } else { 0.0 @@ -243,32 +248,32 @@ where let state = tree.state.downcast_mut::(); if let Some(origin) = state.cursor_grabbed_at { - let image_size = image_size( + let scaled_size = scaled_image_size( renderer, &self.handle, state, bounds.size(), self.content_fit, ); - let hidden_width = (image_size.width - bounds.width / 2.0) + let hidden_width = (scaled_size.width - bounds.width / 2.0) .max(0.0) .round(); - let hidden_height = (image_size.height + let hidden_height = (scaled_size.height - bounds.height / 2.0) .max(0.0) .round(); let delta = position - origin; - let x = if bounds.width < image_size.width { + let x = if bounds.width < scaled_size.width { (state.starting_offset.x - delta.x) .clamp(-hidden_width, hidden_width) } else { 0.0 }; - let y = if bounds.height < image_size.height { + let y = if bounds.height < scaled_size.height { (state.starting_offset.y - delta.y) .clamp(-hidden_height, hidden_height) } else { @@ -320,7 +325,7 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let image_size = image_size( + let image_size = scaled_image_size( renderer, &self.handle, state, @@ -329,21 +334,22 @@ where ); let translation = { - let image_top_left = Vector::new( - (bounds.width - image_size.width).max(0.0) / 2.0, - (bounds.height - image_size.height).max(0.0) / 2.0, - ); + let diff_w = bounds.width - image_size.width; + let diff_h = bounds.height - image_size.height; + + let image_top_left = match self.content_fit { + ContentFit::None => { + Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0) + } + _ => Vector::new(diff_w / 2.0, diff_h / 2.0), + }; image_top_left - state.offset(bounds, image_size) }; + let drawing_bounds = Rectangle::new(bounds.position(), image_size); let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { - let drawing_bounds = Rectangle { - width: image_size.width, - height: image_size.height, - ..bounds - }; renderer.draw_image( self.handle.clone(), self.filter_method, @@ -418,7 +424,7 @@ where /// Returns the bounds of the underlying image, given the bounds of /// the [`Viewer`]. Scaling will be applied and original aspect ratio /// will be respected. -pub fn image_size( +pub fn scaled_image_size( renderer: &Renderer, handle: &::Handle, state: &State, @@ -428,9 +434,11 @@ pub fn image_size( where Renderer: image::Renderer, { - let size = renderer.measure_image(handle); - let size = Size::new(size.width as f32, size.height as f32); - let size = content_fit.fit(size, bounds); + let image_size = { + let Size { width, height } = renderer.measure_image(handle); + Size::new(width as f32, height as f32) + }; + let fit_size = content_fit.fit(image_size, bounds); - Size::new(size.width * state.scale, size.height * state.scale) + Size::new(fit_size.width * state.scale, fit_size.height * state.scale) } From f9124470b4b6adca19094dd0f5e663efd9471ec3 Mon Sep 17 00:00:00 2001 From: Skygrango Date: Fri, 3 May 2024 13:04:39 +0800 Subject: [PATCH 010/657] Fix `clock` example doesn't get the correct local time under unix system There is a long-standing problem (https://github.com/time-rs/time/issues/293) that has not yet been solved by time-rs Switch to chrono as it seemed to solve the problem (https://github.com/chronotope/chrono/pull/677) --- examples/clock/Cargo.toml | 3 +-- examples/clock/src/main.rs | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index dc2e5382..2fddc7da 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -8,6 +8,5 @@ publish = false [dependencies] iced.workspace = true iced.features = ["canvas", "tokio", "debug"] - -time = { version = "0.3", features = ["local-offset"] } +chrono = { version = "0.4", features = [ "clock" ] } tracing-subscriber = "0.3" diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index d717db36..3ffc9f07 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -7,6 +7,9 @@ use iced::{ Theme, Vector, }; +use chrono as time; +use time::Timelike; + pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); @@ -18,13 +21,13 @@ pub fn main() -> iced::Result { } struct Clock { - now: time::OffsetDateTime, + now: time::DateTime, clock: Cache, } #[derive(Debug, Clone, Copy)] enum Message { - Tick(time::OffsetDateTime), + Tick(time::DateTime), } impl Clock { @@ -54,16 +57,12 @@ impl Clock { } fn subscription(&self) -> Subscription { - iced::time::every(std::time::Duration::from_millis(500)).map(|_| { - Message::Tick( - time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), - ) - }) + iced::time::every(std::time::Duration::from_millis(500)) + .map(|_| Message::Tick(time::offset::Local::now())) } fn theme(&self) -> Theme { - Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()] + Theme::ALL[(self.now.timestamp() as usize / 10) % Theme::ALL.len()] .clone() } } @@ -71,8 +70,7 @@ impl Clock { impl Default for Clock { fn default() -> Self { Self { - now: time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + now: time::offset::Local::now(), clock: Cache::default(), } } @@ -127,17 +125,17 @@ impl canvas::Program for Clock { frame.translate(Vector::new(center.x, center.y)); frame.with_save(|frame| { - frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.rotate(hand_rotation(self.now.hour() as u8, 12)); frame.stroke(&short_hand, wide_stroke()); }); frame.with_save(|frame| { - frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.rotate(hand_rotation(self.now.minute() as u8, 60)); frame.stroke(&long_hand, wide_stroke()); }); frame.with_save(|frame| { - let rotation = hand_rotation(self.now.second(), 60); + let rotation = hand_rotation(self.now.second() as u8, 60); frame.rotate(rotation); frame.stroke(&long_hand, thin_stroke()); From 22d55ccecbc546996f5fdbc07cff34d1a6d6ee44 Mon Sep 17 00:00:00 2001 From: gigas002 Date: Wed, 8 May 2024 19:18:13 +0900 Subject: [PATCH 011/657] Run cargo fmt --- widget/src/image/viewer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 03a9d895..175ab63d 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians, + Rectangle, Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. From c95e29696251b957552bcfef356bcfabe213b93c Mon Sep 17 00:00:00 2001 From: gigas002 Date: Fri, 10 May 2024 01:31:23 +0000 Subject: [PATCH 012/657] Make viewer widget inner naming and syntax closer to image widget --- widget/src/image/viewer.rs | 52 ++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 175ab63d..b8b69b60 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,6 +1,6 @@ //! Zoom and pan on an image. use crate::core::event::{self, Event}; -use crate::core::image; +use crate::core::image::{self, FilterMethod}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -20,27 +20,27 @@ pub struct Viewer { max_scale: f32, scale_step: f32, handle: Handle, - filter_method: image::FilterMethod, + filter_method: FilterMethod, content_fit: ContentFit, } impl Viewer { /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(handle: Handle) -> Self { + pub fn new>(handle: T) -> Self { Viewer { - handle, + handle: handle.into(), padding: 0.0, width: Length::Shrink, height: Length::Shrink, min_scale: 0.25, max_scale: 10.0, scale_step: 0.10, - filter_method: image::FilterMethod::default(), - content_fit: ContentFit::Contain, + filter_method: FilterMethod::default(), + content_fit: ContentFit::default(), } } - /// Sets the [`image::FilterMethod`] of the [`Viewer`]. + /// Sets the [`FilterMethod`] of the [`Viewer`]. pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self { self.filter_method = filter_method; self @@ -124,25 +124,24 @@ where limits: &layout::Limits, ) -> layout::Node { // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.measure_image(&self.handle); - Size::new(width as f32, height as f32) - }; + let image_size = renderer.measure_image(&self.handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(self.width, self.height, image_size); // The uncropped size of the image when fit to the bounds above - let fit_size = self.content_fit.fit(image_size, raw_size); + let full_size = self.content_fit.fit(image_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { width: match self.width { - Length::Shrink => f32::min(raw_size.width, fit_size.width), + Length::Shrink => f32::min(raw_size.width, full_size.width), _ => raw_size.width, }, height: match self.height { - Length::Shrink => f32::min(raw_size.height, fit_size.height), + Length::Shrink => f32::min(raw_size.height, full_size.height), _ => raw_size.height, }, }; @@ -323,7 +322,7 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let image_size = scaled_image_size( + let final_size = scaled_image_size( renderer, &self.handle, state, @@ -332,8 +331,8 @@ where ); let translation = { - let diff_w = bounds.width - image_size.width; - let diff_h = bounds.height - image_size.height; + let diff_w = bounds.width - final_size.width; + let diff_h = bounds.height - final_size.height; let image_top_left = match self.content_fit { ContentFit::None => { @@ -342,9 +341,10 @@ where _ => Vector::new(diff_w / 2.0, diff_h / 2.0), }; - image_top_left - state.offset(bounds, image_size) + image_top_left - state.offset(bounds, final_size) }; - let drawing_bounds = Rectangle::new(bounds.position(), image_size); + + let drawing_bounds = Rectangle::new(bounds.position(), final_size); let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { @@ -434,11 +434,13 @@ pub fn scaled_image_size( where Renderer: image::Renderer, { - let image_size = { - let Size { width, height } = renderer.measure_image(handle); - Size::new(width as f32, height as f32) - }; - let fit_size = content_fit.fit(image_size, bounds); + let Size { width, height } = renderer.measure_image(handle); + let image_size = Size::new(width as f32, height as f32); - Size::new(fit_size.width * state.scale, fit_size.height * state.scale) + let adjusted_fit = content_fit.fit(image_size, bounds); + + Size::new( + adjusted_fit.width * state.scale, + adjusted_fit.height * state.scale, + ) } From fb23e4c3ff7aec13725aa3814630b436cd94cab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 16 May 2024 17:20:21 +0200 Subject: [PATCH 013/657] Fix main window not closing in multi-window runtime --- winit/src/multi_window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 4cc08d18..95d78b83 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -474,7 +474,7 @@ async fn run_instance( let _ = window_manager.insert( window::Id::MAIN, - main_window.clone(), + main_window, &application, &mut compositor, exit_on_close_request, From d265cc133efbe02cab890260dbce16768f3d06dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 18 May 2024 11:29:41 +0200 Subject: [PATCH 014/657] Simplify `clock` example a bit --- examples/clock/src/main.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 3ffc9f07..7c4685c4 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,5 +1,6 @@ use iced::alignment; use iced::mouse; +use iced::time; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ @@ -7,9 +8,6 @@ use iced::{ Theme, Vector, }; -use chrono as time; -use time::Timelike; - pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); @@ -21,13 +19,13 @@ pub fn main() -> iced::Result { } struct Clock { - now: time::DateTime, + now: chrono::DateTime, clock: Cache, } #[derive(Debug, Clone, Copy)] enum Message { - Tick(time::DateTime), + Tick(chrono::DateTime), } impl Clock { @@ -57,8 +55,8 @@ impl Clock { } fn subscription(&self) -> Subscription { - iced::time::every(std::time::Duration::from_millis(500)) - .map(|_| Message::Tick(time::offset::Local::now())) + time::every(time::Duration::from_millis(500)) + .map(|_| Message::Tick(chrono::offset::Local::now())) } fn theme(&self) -> Theme { @@ -70,7 +68,7 @@ impl Clock { impl Default for Clock { fn default() -> Self { Self { - now: time::offset::Local::now(), + now: chrono::offset::Local::now(), clock: Cache::default(), } } @@ -87,6 +85,8 @@ impl canvas::Program for Clock { bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec { + use chrono::Timelike; + let clock = self.clock.draw(renderer, bounds.size(), |frame| { let palette = theme.extended_palette(); @@ -125,17 +125,17 @@ impl canvas::Program for Clock { frame.translate(Vector::new(center.x, center.y)); frame.with_save(|frame| { - frame.rotate(hand_rotation(self.now.hour() as u8, 12)); + frame.rotate(hand_rotation(self.now.hour(), 12)); frame.stroke(&short_hand, wide_stroke()); }); frame.with_save(|frame| { - frame.rotate(hand_rotation(self.now.minute() as u8, 60)); + frame.rotate(hand_rotation(self.now.minute(), 60)); frame.stroke(&long_hand, wide_stroke()); }); frame.with_save(|frame| { - let rotation = hand_rotation(self.now.second() as u8, 60); + let rotation = hand_rotation(self.now.second(), 60); frame.rotate(rotation); frame.stroke(&long_hand, thin_stroke()); @@ -167,7 +167,7 @@ impl canvas::Program for Clock { } } -fn hand_rotation(n: u8, total: u8) -> Degrees { +fn hand_rotation(n: u32, total: u32) -> Degrees { let turns = n as f32 / total as f32; Degrees(360.0 * turns) From 4936efc3751b769984bff4344a9fbb198a7c1ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 18 May 2024 11:32:26 +0200 Subject: [PATCH 015/657] Remove redundant default `chrono` feature in `clock` example --- examples/clock/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 2fddc7da..bc6c202b 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -8,5 +8,5 @@ publish = false [dependencies] iced.workspace = true iced.features = ["canvas", "tokio", "debug"] -chrono = { version = "0.4", features = [ "clock" ] } +chrono = "0.4" tracing-subscriber = "0.3" From d92e0f7bba8959384467048c7eca84b4b8a7195f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 22 May 2024 12:29:31 +0200 Subject: [PATCH 016/657] Fix compilation of `integration` example in `release` mode Fixes #2447. --- examples/integration/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index a4a961f8..7f8feb3f 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -8,7 +8,9 @@ publish = false [dependencies] iced_winit.workspace = true iced_wgpu.workspace = true + iced_widget.workspace = true +iced_widget.features = ["wgpu"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = "0.3" From 468794d918eb06c1dbebb33c32b10017ad335f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 22 May 2024 12:36:04 +0200 Subject: [PATCH 017/657] Produce a compile error in `iced_renderer` when no backend is enabled --- renderer/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 056da5ed..220542e1 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -48,6 +48,13 @@ mod renderer { #[cfg(not(any(feature = "wgpu", feature = "tiny-skia")))] mod renderer { + #[cfg(not(debug_assertions))] + compile_error!( + "Cannot compile `iced_renderer` in release mode \ + without a renderer feature enabled. \ + Enable either the `wgpu` or `tiny-skia` feature, or both." + ); + pub type Renderer = (); pub type Compositor = (); } From 647761ad56fbae20c5299296f88c7c88db65c07c Mon Sep 17 00:00:00 2001 From: Shan Date: Fri, 24 May 2024 19:46:18 -0700 Subject: [PATCH 018/657] Added scale_factor to `Screenshot` data for use when cropping to widget bounds --- runtime/src/window/screenshot.rs | 16 +++++++++++++--- winit/src/application.rs | 1 + winit/src/multi_window.rs | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index fb318110..d9adbc01 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -11,16 +11,20 @@ use std::fmt::{Debug, Formatter}; pub struct Screenshot { /// The bytes of the [`Screenshot`]. pub bytes: Bytes, - /// The size of the [`Screenshot`]. + /// The size of the [`Screenshot`] in physical pixels. pub size: Size, + /// The scale factor of the [`Screenshot`]. This can be useful when converting between widget + /// bounds (which are in logical pixels) to crop screenshots. + pub scale_factor: f64, } impl Debug for Screenshot { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "Screenshot: {{ \n bytes: {}\n size: {:?} }}", + "Screenshot: {{ \n bytes: {}\n scale: {}\n size: {:?} }}", self.bytes.len(), + self.scale_factor, self.size ) } @@ -28,10 +32,15 @@ impl Debug for Screenshot { impl Screenshot { /// Creates a new [`Screenshot`]. - pub fn new(bytes: impl Into, size: Size) -> Self { + pub fn new( + bytes: impl Into, + size: Size, + scale_factor: f64, + ) -> Self { Self { bytes: bytes.into(), size, + scale_factor, } } @@ -70,6 +79,7 @@ impl Screenshot { Ok(Self { bytes: Bytes::from(chopped), size: Size::new(region.width, region.height), + scale_factor: self.scale_factor, }) } } diff --git a/winit/src/application.rs b/winit/src/application.rs index f7508b4c..4aed1eee 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1066,6 +1066,7 @@ pub fn run_command( proxy.send(tag(window::Screenshot::new( bytes, state.physical_size(), + state.viewport().scale_factor(), ))); } }, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 95d78b83..74ab64f2 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1239,6 +1239,7 @@ fn run_command( proxy.send(tag(window::Screenshot::new( bytes, window.state.physical_size(), + window.state.viewport().scale_factor(), ))); } } From 07f94d68b51a4ca1b6ccad1f216a372b6460035d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 27 May 2024 13:47:57 +0200 Subject: [PATCH 019/657] Update outdated `README`s of subcrates --- core/README.md | 12 ------------ wgpu/README.md | 35 +---------------------------------- winit/README.md | 12 ------------ 3 files changed, 1 insertion(+), 58 deletions(-) diff --git a/core/README.md b/core/README.md index 519e0608..de11acad 100644 --- a/core/README.md +++ b/core/README.md @@ -13,15 +13,3 @@ This crate is meant to be a starting point for an Iced runtime.

[documentation]: https://docs.rs/iced_core - -## Installation -Add `iced_core` as a dependency in your `Cargo.toml`: - -```toml -iced_core = "0.9" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases diff --git a/wgpu/README.md b/wgpu/README.md index 95d7028a..8e9602ea 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -6,14 +6,7 @@ `iced_wgpu` is a [`wgpu`] renderer for [`iced_runtime`]. For now, it is the default renderer of Iced on [native platforms]. -[`wgpu`] supports most modern graphics backends: Vulkan, Metal, and DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the incoming [WebGPU API]. - -Currently, `iced_wgpu` supports the following primitives: -- Text, which is rendered using [`wgpu_glyph`]. No shaping at all. -- Quads or rectangles, with rounded borders and a solid background color. -- Clip areas, useful to implement scrollables or hide overflowing content. -- Images and SVG, loaded from memory or the file system. -- Meshes of triangles, useful to draw geometry freely. +[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX12, OpenGL, and WebGPU.

The native target @@ -25,29 +18,3 @@ Currently, `iced_wgpu` supports the following primitives: [native platforms]: https://github.com/gfx-rs/wgpu#supported-platforms [WebGPU API]: https://gpuweb.github.io/gpuweb/ [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph - -## Installation -Add `iced_wgpu` as a dependency in your `Cargo.toml`: - -```toml -iced_wgpu = "0.10" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases - -## Current limitations - -The current implementation is quite naive; it uses: - -- A different pipeline/shader for each primitive -- A very simplistic layer model: every `Clip` primitive will generate new layers -- _Many_ render passes instead of preparing everything upfront -- A glyph cache that is trimmed incorrectly when there are multiple layers (a [`glyph_brush`] limitation) - -Some of these issues are already being worked on! If you want to help, [get in touch!] - -[get in touch!]: ../CONTRIBUTING.md -[`glyph_brush`]: https://github.com/alexheretic/glyph-brush diff --git a/winit/README.md b/winit/README.md index 91307970..c60e81f9 100644 --- a/winit/README.md +++ b/winit/README.md @@ -15,15 +15,3 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t [documentation]: https://docs.rs/iced_winit [`iced_native`]: ../native [`winit`]: https://github.com/rust-windowing/winit - -## Installation -Add `iced_winit` as a dependency in your `Cargo.toml`: - -```toml -iced_winit = "0.9" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases From 37aff42e4711199eef64079636ff808c6fe3124b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 27 May 2024 21:31:20 +0200 Subject: [PATCH 020/657] Remove "Installation" section from the `README` --- README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.md b/README.md index 0db09ded..bd70862f 100644 --- a/README.md +++ b/README.md @@ -63,22 +63,6 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], [check out the issues]: https://github.com/iced-rs/iced/issues [feel free to contribute!]: #contributing--feedback -## Installation - -Add `iced` as a dependency in your `Cargo.toml`: - -```toml -iced = "0.12" -``` - -If your project is using a Rust edition older than 2021, then you will need to -set `resolver = "2"` in the `[package]` section as well. - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases - ## Overview Inspired by [The Elm Architecture], Iced expects you to split user interfaces From 0fbbfdfbef7354e419c3ae7332ae3d392572ce78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 27 May 2024 21:33:00 +0200 Subject: [PATCH 021/657] Reference "Examples" header in examples link in the `README` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd70862f..a9c37977 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com] [book]: https://book.iced.rs/ [documentation]: https://docs.rs/iced/ -[examples]: https://github.com/iced-rs/iced/tree/master/examples +[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples [Coffee]: https://github.com/hecrj/coffee [Elm]: https://elm-lang.org/ [The Elm Architecture]: https://guide.elm-lang.org/architecture/ From 12f4b875cfb0eeb0bac5416921c6cec40edd420c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 27 May 2024 21:44:44 +0200 Subject: [PATCH 022/657] Link to the `latest` branch in examples' `README` --- examples/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index 71dad13e..232b6042 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,6 @@ # Examples -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases +__Iced moves fast and the `master` branch can contain breaking changes!__ If you want to browse examples that are compatible with the latest release, +then [switch to the `latest` branch](https://github.com/iced-rs/iced/tree/latest/examples#examples). ## [Tour](tour) A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. From bd48946b02268cb5a7ae932e388abfa75fe9e375 Mon Sep 17 00:00:00 2001 From: BradySimon Date: Fri, 8 Mar 2024 09:35:43 -0500 Subject: [PATCH 023/657] Add Command + ArrowLeft/Right input behavior for macos --- widget/src/text_editor.rs | 37 +++++++++++++++--- widget/src/text_input.rs | 81 +++++++++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 7c0b98ea..3b6647e4 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -767,7 +767,7 @@ impl Update { } if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key) { + if let Some(motion) = motion(named_key, modifiers) { let motion = if platform::is_jump_modifier_pressed( modifiers, ) { @@ -793,14 +793,26 @@ impl Update { } } -fn motion(key: key::Named) -> Option { +fn motion(key: key::Named, modifiers: keyboard::Modifiers) -> Option { match key { - key::Named::ArrowLeft => Some(Motion::Left), - key::Named::ArrowRight => Some(Motion::Right), - key::Named::ArrowUp => Some(Motion::Up), - key::Named::ArrowDown => Some(Motion::Down), key::Named::Home => Some(Motion::Home), key::Named::End => Some(Motion::End), + key::Named::ArrowLeft => { + if platform::is_macos_command_pressed(modifiers) { + Some(Motion::Home) + } else { + Some(Motion::Left) + } + } + key::Named::ArrowRight => { + if platform::is_macos_command_pressed(modifiers) { + Some(Motion::End) + } else { + Some(Motion::Right) + } + } + key::Named::ArrowUp => Some(Motion::Up), + key::Named::ArrowDown => Some(Motion::Down), key::Named::PageUp => Some(Motion::PageUp), key::Named::PageDown => Some(Motion::PageDown), _ => None, @@ -817,6 +829,19 @@ mod platform { modifiers.control() } } + + /// Whether the command key is pressed on a macOS device. + /// + /// This is relevant for actions like ⌘ + ArrowLeft to move to the beginning of the + /// line where the equivalent behavior for `modifiers.command()` is instead a jump on + /// other platforms. + pub fn is_macos_command_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.logo() + } else { + false + } + } } /// The possible status of a [`TextEditor`]. diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index e9f07838..2c951c12 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -876,6 +876,54 @@ where update_cache(state, &self.value); } + keyboard::Key::Named(key::Named::Home) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::End) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } + keyboard::Key::Named(key::Named::ArrowLeft) + if platform::is_macos_command_pressed( + modifiers, + ) => + { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::ArrowRight) + if platform::is_macos_command_pressed( + modifiers, + ) => + { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } keyboard::Key::Named(key::Named::ArrowLeft) => { if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure @@ -914,26 +962,6 @@ where state.cursor.move_right(&self.value); } } - keyboard::Key::Named(key::Named::Home) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(&self.value), - 0, - ); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(key::Named::End) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(&self.value), - self.value.len(), - ); - } else { - state.cursor.move_to(self.value.len()); - } - } keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; state.is_dragging = false; @@ -1291,6 +1319,19 @@ mod platform { modifiers.control() } } + + /// Whether the command key is pressed on a macOS device. + /// + /// This is relevant for actions like ⌘ + ArrowLeft to move to the beginning of the + /// line where the equivalent behavior for `modifiers.command()` is instead a jump on + /// other platforms. + pub fn is_macos_command_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.logo() + } else { + false + } + } } fn offset( From 8cfa8149f56a0e58eb0257ebb181c69bec5c095f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 31 May 2024 16:10:59 +0200 Subject: [PATCH 024/657] Keep unary `motion` function in `text_editor` --- widget/src/text_editor.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 3b6647e4..9e494394 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -767,7 +767,19 @@ impl Update { } if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key, modifiers) { + if let Some(motion) = motion(named_key) { + let motion = if platform::is_macos_command_pressed( + modifiers, + ) { + match motion { + Motion::Left => Motion::Home, + Motion::Right => Motion::End, + _ => motion, + } + } else { + motion + }; + let motion = if platform::is_jump_modifier_pressed( modifiers, ) { @@ -793,26 +805,14 @@ impl Update { } } -fn motion(key: key::Named, modifiers: keyboard::Modifiers) -> Option { +fn motion(key: key::Named) -> Option { match key { - key::Named::Home => Some(Motion::Home), - key::Named::End => Some(Motion::End), - key::Named::ArrowLeft => { - if platform::is_macos_command_pressed(modifiers) { - Some(Motion::Home) - } else { - Some(Motion::Left) - } - } - key::Named::ArrowRight => { - if platform::is_macos_command_pressed(modifiers) { - Some(Motion::End) - } else { - Some(Motion::Right) - } - } + key::Named::ArrowLeft => Some(Motion::Left), + key::Named::ArrowRight => Some(Motion::Right), key::Named::ArrowUp => Some(Motion::Up), key::Named::ArrowDown => Some(Motion::Down), + key::Named::Home => Some(Motion::Home), + key::Named::End => Some(Motion::End), key::Named::PageUp => Some(Motion::PageUp), key::Named::PageDown => Some(Motion::PageDown), _ => None, From 3312dc808012e2c049fb2f178d51bfe0b4e30399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 31 May 2024 16:23:09 +0200 Subject: [PATCH 025/657] Create `jump` and `macos_command` methods in `keyboard::Modifiers` --- core/src/keyboard/modifiers.rs | 24 ++++++++++++++++++ widget/src/text_editor.rs | 33 ++----------------------- widget/src/text_input.rs | 45 +++++----------------------------- 3 files changed, 32 insertions(+), 70 deletions(-) diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index e531510f..edbf6d38 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -84,4 +84,28 @@ impl Modifiers { is_pressed } + + /// Returns true if the "jump key" is pressed in the [`Modifiers`]. + /// + /// The "jump key" is the modifier key used to widen text motions. It is the `Alt` + /// key in macOS and the `Ctrl` key in other platforms. + pub fn jump(self) -> bool { + if cfg!(target_os = "macos") { + self.alt() + } else { + self.control() + } + } + + /// Returns true if the "command key" is pressed on a macOS device. + /// + /// This is relevant for macOS-specific actions (e.g. `⌘ + ArrowLeft` moves the cursor + /// to the beginning of the line). + pub fn macos_command(self) -> bool { + if cfg!(target_os = "macos") { + self.logo() + } else { + false + } + } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 9e494394..41b058af 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -768,9 +768,7 @@ impl Update { if let keyboard::Key::Named(named_key) = key.as_ref() { if let Some(motion) = motion(named_key) { - let motion = if platform::is_macos_command_pressed( - modifiers, - ) { + let motion = if modifiers.macos_command() { match motion { Motion::Left => Motion::Home, Motion::Right => Motion::End, @@ -780,9 +778,7 @@ impl Update { motion }; - let motion = if platform::is_jump_modifier_pressed( - modifiers, - ) { + let motion = if modifiers.jump() { motion.widen() } else { motion @@ -819,31 +815,6 @@ fn motion(key: key::Named) -> Option { } } -mod platform { - use crate::core::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } - - /// Whether the command key is pressed on a macOS device. - /// - /// This is relevant for actions like ⌘ + ArrowLeft to move to the beginning of the - /// line where the equivalent behavior for `modifiers.command()` is instead a jump on - /// other platforms. - pub fn is_macos_command_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.logo() - } else { - false - } - } -} - /// The possible status of a [`TextEditor`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 2c951c12..941e9bde 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -826,7 +826,7 @@ where } } keyboard::Key::Named(key::Named::Backspace) => { - if platform::is_jump_modifier_pressed(modifiers) + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { if self.is_secure { @@ -850,7 +850,7 @@ where update_cache(state, &self.value); } keyboard::Key::Named(key::Named::Delete) => { - if platform::is_jump_modifier_pressed(modifiers) + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { if self.is_secure { @@ -897,9 +897,7 @@ where } } keyboard::Key::Named(key::Named::ArrowLeft) - if platform::is_macos_command_pressed( - modifiers, - ) => + if modifiers.macos_command() => { if modifiers.shift() { state.cursor.select_range( @@ -911,9 +909,7 @@ where } } keyboard::Key::Named(key::Named::ArrowRight) - if platform::is_macos_command_pressed( - modifiers, - ) => + if modifiers.macos_command() => { if modifiers.shift() { state.cursor.select_range( @@ -925,9 +921,7 @@ where } } keyboard::Key::Named(key::Named::ArrowLeft) => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state .cursor @@ -944,9 +938,7 @@ where } } keyboard::Key::Named(key::Named::ArrowRight) => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state .cursor @@ -1309,31 +1301,6 @@ impl operation::TextInput for State

{ } } -mod platform { - use crate::core::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } - - /// Whether the command key is pressed on a macOS device. - /// - /// This is relevant for actions like ⌘ + ArrowLeft to move to the beginning of the - /// line where the equivalent behavior for `modifiers.command()` is instead a jump on - /// other platforms. - pub fn is_macos_command_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.logo() - } else { - false - } - } -} - fn offset( text_bounds: Rectangle, value: &Value, From e2b00f98a0fab96da6502610b135a4c86fbd63b5 Mon Sep 17 00:00:00 2001 From: PolyMeilex Date: Sat, 8 Jun 2024 02:09:10 +0200 Subject: [PATCH 026/657] Allow for styling of the menu of a pick list --- widget/src/pick_list.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index edccfdaa..97de5b48 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -161,6 +161,19 @@ where self } + /// Sets the style of the [`Menu`]. + #[must_use] + pub fn menu_style( + mut self, + style: impl Fn(&Theme) -> menu::Style + 'a, + ) -> Self + where + ::Class<'a>: From>, + { + self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into(); + self + } + /// Sets the style class of the [`PickList`]. #[cfg(feature = "advanced")] #[must_use] @@ -171,6 +184,17 @@ where self.class = class.into(); self } + + /// Sets the style class of the [`Menu`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn menu_class( + mut self, + class: impl Into<::Class<'a>>, + ) -> Self { + self.menu_class = class.into(); + self + } } impl<'a, T, L, V, Message, Theme, Renderer> Widget From 49affc44ff57ad879a73d9b4d329863d6f4b1d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 10 Jun 2024 22:01:23 +0200 Subject: [PATCH 027/657] Update `winit` to `0.30.1` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fc35fee8..44b3d307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,7 +176,7 @@ web-time = "1.1" wgpu = "0.19" winapi = "0.3" window_clipboard = "0.4.1" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "8affa522bc6dcc497d332a28c03491d22a22f5a7" } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } [workspace.lints.rust] rust_2018_idioms = "forbid" From e400f972c1fe6fa4f70f8cfe559ded680e6cf740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 4 Jun 2024 23:20:33 +0200 Subject: [PATCH 028/657] Introduce `window::Id` to `Event` subscriptions And remove `window::Id` from `Event` altogether. --- core/src/event.rs | 2 +- examples/events/src/main.rs | 5 ++-- examples/integration/src/main.rs | 2 -- examples/loading_spinners/src/circular.rs | 2 +- examples/loading_spinners/src/linear.rs | 2 +- examples/multi_window/src/main.rs | 12 ++++---- examples/toast/src/main.rs | 4 +-- examples/visible_bounds/src/main.rs | 4 +-- futures/src/event.rs | 19 ++++++------ futures/src/keyboard.rs | 11 +++---- futures/src/runtime.rs | 10 +++++-- futures/src/subscription.rs | 11 ++++--- futures/src/subscription/tracker.rs | 16 +++++++++-- runtime/src/window.rs | 4 +-- widget/src/shader.rs | 2 +- widget/src/text_input.rs | 6 ++-- winit/src/application.rs | 10 ++++--- winit/src/conversion.rs | 35 +++++++++-------------- winit/src/multi_window.rs | 29 +++++++------------ 19 files changed, 95 insertions(+), 91 deletions(-) diff --git a/core/src/event.rs b/core/src/event.rs index 870b3074..953cd73f 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -19,7 +19,7 @@ pub enum Event { Mouse(mouse::Event), /// A window event - Window(window::Id, window::Event), + Window(window::Event), /// A touch event Touch(touch::Event), diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 9d1c502a..bacd8e6e 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -37,9 +37,8 @@ impl Events { Command::none() } Message::EventOccurred(event) => { - if let Event::Window(id, window::Event::CloseRequested) = event - { - window::close(id) + if let Event::Window(window::Event::CloseRequested) = event { + window::close(window::Id::MAIN) } else { Command::none() } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index e1c7d62f..9818adf3 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -9,7 +9,6 @@ use iced_wgpu::{wgpu, Engine, Renderer}; use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; -use iced_winit::core::window; use iced_winit::core::{Color, Font, Pixels, Size, Theme}; use iced_winit::futures; use iced_winit::runtime::program; @@ -317,7 +316,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( - window::Id::MAIN, event, window.scale_factor(), *modifiers, diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index de728af2..bf70e190 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -275,7 +275,7 @@ where ) -> event::Status { let state = tree.state.downcast_mut::(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index ce375621..164993c6 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -189,7 +189,7 @@ where ) -> event::Status { let state = tree.state.downcast_mut::(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); shell.request_redraw(RedrawRequest::NextFrame); diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 31c2e4f6..eb74c94a 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -145,16 +145,18 @@ impl multi_window::Application for Example { } fn subscription(&self) -> Subscription { - event::listen_with(|event, _| { - if let iced::Event::Window(id, window_event) = event { + event::listen_with(|event, _, window| { + if let iced::Event::Window(window_event) = event { match window_event { window::Event::CloseRequested => { - Some(Message::CloseWindow(id)) + Some(Message::CloseWindow(window)) } window::Event::Opened { position, .. } => { - Some(Message::WindowOpened(id, position)) + Some(Message::WindowOpened(window, position)) + } + window::Event::Closed => { + Some(Message::WindowClosed(window)) } - window::Event::Closed => Some(Message::WindowClosed(id)), _ => None, } } else { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 355c40b8..700b6b10 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -499,9 +499,7 @@ mod toast { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let Event::Window(_, window::Event::RedrawRequested(now)) = - &event - { + if let Event::Window(window::Event::RedrawRequested(now)) = &event { let mut next_redraw: Option = None; self.instants.iter_mut().enumerate().for_each( diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 332b6a7b..8030f5b4 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -145,11 +145,11 @@ impl Example { } fn subscription(&self) -> Subscription { - event::listen_with(|event, _| match event { + event::listen_with(|event, _status, _window| match event { Event::Mouse(mouse::Event::CursorMoved { position }) => { Some(Message::MouseMoved(position)) } - Event::Window(_, window::Event::Resized { .. }) => { + Event::Window(window::Event::Resized { .. }) => { Some(Message::WindowResized) } _ => None, diff --git a/futures/src/event.rs b/futures/src/event.rs index 97224506..4f3342ca 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -9,7 +9,7 @@ use crate::MaybeSend; /// This subscription will notify your application of any [`Event`] that was /// not captured by any widget. pub fn listen() -> Subscription { - listen_with(|event, status| match status { + listen_with(|event, status, _window| match status { event::Status::Ignored => Some(event), event::Status::Captured => None, }) @@ -24,7 +24,7 @@ pub fn listen() -> Subscription { /// - Returns `None`, the [`Event`] will be discarded. /// - Returns `Some` message, the `Message` will be produced. pub fn listen_with( - f: fn(Event, event::Status) -> Option, + f: fn(Event, event::Status, window::Id) -> Option, ) -> Subscription where Message: 'static + MaybeSend, @@ -32,13 +32,12 @@ where #[derive(Hash)] struct EventsWith; - subscription::filter_map( - (EventsWith, f), - move |event, status| match event { - Event::Window(_, window::Event::RedrawRequested(_)) => None, - _ => f(event, status), - }, - ) + subscription::filter_map((EventsWith, f), move |event, status, window| { + match event { + Event::Window(window::Event::RedrawRequested(_)) => None, + _ => f(event, status, window), + } + }) } /// Creates a [`Subscription`] that produces a message for every runtime event, @@ -47,7 +46,7 @@ where /// **Warning:** This [`Subscription`], if unfiltered, may produce messages in /// an infinite loop. pub fn listen_raw( - f: fn(Event, event::Status) -> Option, + f: fn(Event, event::Status, window::Id) -> Option, ) -> Subscription where Message: 'static + MaybeSend, diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index 8e7da38f..43ed7742 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -18,7 +18,7 @@ where #[derive(Hash)] struct OnKeyPress; - subscription::filter_map((OnKeyPress, f), move |event, status| { + subscription::filter_map((OnKeyPress, f), move |event, status, _window| { match (event, status) { ( core::Event::Keyboard(Event::KeyPressed { @@ -45,8 +45,9 @@ where #[derive(Hash)] struct OnKeyRelease; - subscription::filter_map((OnKeyRelease, f), move |event, status| { - match (event, status) { + subscription::filter_map( + (OnKeyRelease, f), + move |event, status, _window| match (event, status) { ( core::Event::Keyboard(Event::KeyReleased { key, @@ -56,6 +57,6 @@ where core::event::Status::Ignored, ) => f(key, modifiers), _ => None, - } - }) + }, + ) } diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index cac7b7e1..ae55f814 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,5 +1,6 @@ //! Run commands and keep track of subscriptions. use crate::core::event::{self, Event}; +use crate::core::window; use crate::subscription; use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; @@ -127,7 +128,12 @@ where /// See [`Tracker::broadcast`] to learn more. /// /// [`Tracker::broadcast`]: subscription::Tracker::broadcast - pub fn broadcast(&mut self, event: Event, status: event::Status) { - self.subscriptions.broadcast(event, status); + pub fn broadcast( + &mut self, + event: Event, + status: event::Status, + window: window::Id, + ) { + self.subscriptions.broadcast(event, status, window); } } diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 93e35608..79cea6ed 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -4,6 +4,7 @@ mod tracker; pub use tracker::Tracker; use crate::core::event::{self, Event}; +use crate::core::window; use crate::futures::{Future, Stream}; use crate::{BoxStream, MaybeSend}; @@ -15,7 +16,7 @@ use std::hash::Hash; /// A stream of runtime events. /// /// It is the input of a [`Subscription`]. -pub type EventStream = BoxStream<(Event, event::Status)>; +pub type EventStream = BoxStream<(Event, event::Status, window::Id)>; /// The hasher used for identifying subscriptions. pub type Hasher = rustc_hash::FxHasher; @@ -289,7 +290,9 @@ where pub(crate) fn filter_map(id: I, f: F) -> Subscription where I: Hash + 'static, - F: Fn(Event, event::Status) -> Option + MaybeSend + 'static, + F: Fn(Event, event::Status, window::Id) -> Option + + MaybeSend + + 'static, Message: 'static + MaybeSend, { Subscription::from_recipe(Runner { @@ -298,8 +301,8 @@ where use futures::future; use futures::stream::StreamExt; - events.filter_map(move |(event, status)| { - future::ready(f(event, status)) + events.filter_map(move |(event, status, window)| { + future::ready(f(event, status, window)) }) }, }) diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index 277a446b..086b0f09 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,4 +1,5 @@ use crate::core::event::{self, Event}; +use crate::core::window; use crate::subscription::{Hasher, Recipe}; use crate::{BoxFuture, MaybeSend}; @@ -23,7 +24,9 @@ pub struct Tracker { #[derive(Debug)] pub struct Execution { _cancel: futures::channel::oneshot::Sender<()>, - listener: Option>, + listener: Option< + futures::channel::mpsc::Sender<(Event, event::Status, window::Id)>, + >, } impl Tracker { @@ -139,12 +142,19 @@ impl Tracker { /// currently open. /// /// [`Recipe::stream`]: crate::subscription::Recipe::stream - pub fn broadcast(&mut self, event: Event, status: event::Status) { + pub fn broadcast( + &mut self, + event: Event, + status: event::Status, + window: window::Id, + ) { self.subscriptions .values_mut() .filter_map(|connection| connection.listener.as_mut()) .for_each(|listener| { - if let Err(error) = listener.try_send((event.clone(), status)) { + if let Err(error) = + listener.try_send((event.clone(), status, window)) + { log::warn!( "Error sending event to subscription: {error:?}" ); diff --git a/runtime/src/window.rs b/runtime/src/window.rs index e32465d3..b68c9a71 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -28,8 +28,8 @@ use raw_window_handle::WindowHandle; /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. pub fn frames() -> Subscription { - event::listen_raw(|event, _status| match event { - crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + event::listen_raw(|event, _status, _window| match event { + crate::core::Event::Window(Event::RedrawRequested(at)) => Some(at), _ => None, }) } diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fad2f4eb..473cfd0d 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -107,7 +107,7 @@ where Some(Event::Keyboard(keyboard_event)) } core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Window(_, window::Event::RedrawRequested(instant)) => { + core::Event::Window(window::Event::RedrawRequested(instant)) => { Some(Event::RedrawRequested(instant)) } _ => None, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 941e9bde..dc4f83e0 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1003,14 +1003,14 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(_, window::Event::Unfocused) => { + Event::Window(window::Event::Unfocused) => { let state = state::(tree); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = false; } } - Event::Window(_, window::Event::Focused) => { + Event::Window(window::Event::Focused) => { let state = state::(tree); if let Some(focus) = &mut state.is_focused { @@ -1020,7 +1020,7 @@ where shell.request_redraw(window::RedrawRequest::NextFrame); } } - Event::Window(_, window::Event::RedrawRequested(now)) => { + Event::Window(window::Event::RedrawRequested(now)) => { let state = state::(tree); if let Some(focus) = &mut state.is_focused { diff --git a/winit/src/application.rs b/winit/src/application.rs index 4aed1eee..ed8715d8 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -623,7 +623,6 @@ async fn run_instance( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = Event::Window( - window::Id::MAIN, window::Event::RedrawRequested(Instant::now()), ); @@ -651,7 +650,11 @@ async fn run_instance( _ => ControlFlow::Wait, }); - runtime.broadcast(redraw_event, core::event::Status::Ignored); + runtime.broadcast( + redraw_event, + core::event::Status::Ignored, + window::Id::MAIN, + ); debug.draw_started(); let new_mouse_interaction = user_interface.draw( @@ -714,7 +717,6 @@ async fn run_instance( state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( - window::Id::MAIN, window_event, state.scale_factor(), state.modifiers(), @@ -742,7 +744,7 @@ async fn run_instance( for (event, status) in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event, status); + runtime.broadcast(event, status, window::Id::MAIN); } if !messages.is_empty() diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index ea33e610..79fcf92e 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -126,7 +126,6 @@ pub fn window_attributes( /// Converts a winit window event into an iced event. pub fn window_event( - id: window::Id, event: winit::event::WindowEvent, scale_factor: f64, modifiers: winit::keyboard::ModifiersState, @@ -137,16 +136,13 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window( - id, - window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - }, - )) + Some(Event::Window(window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + })) } WindowEvent::CloseRequested => { - Some(Event::Window(id, window::Event::CloseRequested)) + Some(Event::Window(window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::(scale_factor); @@ -264,22 +260,19 @@ pub fn window_event( self::modifiers(new_modifiers.state()), ))) } - WindowEvent::Focused(focused) => Some(Event::Window( - id, - if focused { - window::Event::Focused - } else { - window::Event::Unfocused - }, - )), + WindowEvent::Focused(focused) => Some(Event::Window(if focused { + window::Event::Focused + } else { + window::Event::Unfocused + })), WindowEvent::HoveredFile(path) => { - Some(Event::Window(id, window::Event::FileHovered(path.clone()))) + Some(Event::Window(window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(id, window::Event::FileDropped(path.clone()))) + Some(Event::Window(window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(id, window::Event::FilesHoveredLeft)) + Some(Event::Window(window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(touch, scale_factor))) @@ -288,7 +281,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(id, window::Event::Moved { x, y })) + Some(Event::Window(window::Event::Moved { x, y })) } _ => None, } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 74ab64f2..3696f952 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -492,13 +492,10 @@ async fn run_instance( let mut events = { vec![( Some(window::Id::MAIN), - core::Event::Window( - window::Id::MAIN, - window::Event::Opened { - position: main_window.position(), - size: main_window.size(), - }, - ), + core::Event::Window(window::Event::Opened { + position: main_window.position(), + size: main_window.size(), + }), )] }; @@ -565,13 +562,10 @@ async fn run_instance( events.push(( Some(id), - core::Event::Window( - id, - window::Event::Opened { - position: window.position(), - size: window.size(), - }, - ), + core::Event::Window(window::Event::Opened { + position: window.position(), + size: window.size(), + }), )); } Event::EventLoopAwakened(event) => { @@ -623,7 +617,6 @@ async fn run_instance( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = core::Event::Window( - id, window::Event::RedrawRequested(Instant::now()), ); @@ -665,6 +658,7 @@ async fn run_instance( runtime.broadcast( redraw_event.clone(), core::event::Status::Ignored, + id, ); let _ = control_sender.start_send(Control::ChangeFlow( @@ -802,7 +796,7 @@ async fn run_instance( events.push(( None, - core::Event::Window(id, window::Event::Closed), + core::Event::Window(window::Event::Closed), )); if window_manager.is_empty() { @@ -816,7 +810,6 @@ async fn run_instance( ); if let Some(event) = conversion::window_event( - id, window_event, window.state.scale_factor(), window.state.modifiers(), @@ -874,7 +867,7 @@ async fn run_instance( .into_iter() .zip(statuses.into_iter()) { - runtime.broadcast(event, status); + runtime.broadcast(event, status, id); } } From ae2bf8ee40d1eb8f8176eae4541550fa2365f7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 6 Jun 2024 02:13:06 +0200 Subject: [PATCH 029/657] Broadcast orphaned events in `multi_window` runtime --- winit/src/multi_window.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 3696f952..b4c30a20 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -491,7 +491,7 @@ async fn run_instance( let mut clipboard = Clipboard::connect(&main_window.raw); let mut events = { vec![( - Some(window::Id::MAIN), + window::Id::MAIN, core::Event::Window(window::Event::Opened { position: main_window.position(), size: main_window.size(), @@ -561,7 +561,7 @@ async fn run_instance( let _ = ui_caches.insert(id, user_interface::Cache::default()); events.push(( - Some(id), + id, core::Event::Window(window::Event::Opened { position: window.position(), size: window.size(), @@ -588,7 +588,7 @@ async fn run_instance( use crate::core::event; events.push(( - None, + window::Id::MAIN, event::Event::PlatformSpecific( event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), @@ -795,7 +795,7 @@ async fn run_instance( let _ = ui_caches.remove(&id); events.push(( - None, + id, core::Event::Window(window::Event::Closed), )); @@ -814,7 +814,7 @@ async fn run_instance( window.state.scale_factor(), window.state.modifiers(), ) { - events.push((Some(id), event)); + events.push((id, event)); } } } @@ -830,8 +830,7 @@ async fn run_instance( let mut window_events = vec![]; events.retain(|(window_id, event)| { - if *window_id == Some(id) || window_id.is_none() - { + if *window_id == id { window_events.push(event.clone()); false } else { @@ -871,6 +870,14 @@ async fn run_instance( } } + for (id, event) in events.drain(..) { + runtime.broadcast( + event, + core::event::Status::Ignored, + id, + ); + } + debug.event_processing_finished(); // TODO mw application update returns which window IDs to update From 83296a73ebbb3c02ed63dfb4661056a8a8962267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 7 Jun 2024 01:59:17 +0200 Subject: [PATCH 030/657] Fix widget operations in `multi_window` runtime --- winit/src/multi_window.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index b4c30a20..13839828 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1272,25 +1272,20 @@ fn run_command( std::mem::take(ui_caches), ); - 'operate: while let Some(mut operation) = - current_operation.take() - { + while let Some(mut operation) = current_operation.take() { for (id, ui) in uis.iter_mut() { if let Some(window) = window_manager.get_mut(*id) { ui.operate(&window.renderer, operation.as_mut()); + } + } - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - - // operation completed, don't need to try to operate on rest of UIs - break 'operate; - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy.send(message); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); } } } From 5d7dcf417c694853a606b8fb0a47a580277fc9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 11 Jun 2024 19:41:05 +0200 Subject: [PATCH 031/657] Introduce `subscription::Event` ... and remove `PlatformSpecific` from `Event` --- core/src/event.rs | 21 ------------ core/src/program.rs | 1 + examples/url_handler/src/main.rs | 17 +++------- futures/src/event.rs | 43 ++++++++++++++++++++++--- futures/src/keyboard.rs | 42 +++++++++++------------- futures/src/runtime.rs | 11 ++----- futures/src/subscription.rs | 50 ++++++++++++++++++++++++----- futures/src/subscription/tracker.rs | 19 +++-------- src/lib.rs | 6 ++-- widget/src/canvas.rs | 2 +- widget/src/shader.rs | 2 +- winit/src/application.rs | 29 +++++++++-------- winit/src/multi_window.rs | 42 +++++++++++++----------- 13 files changed, 156 insertions(+), 129 deletions(-) create mode 100644 core/src/program.rs diff --git a/core/src/event.rs b/core/src/event.rs index 953cd73f..b6cf321e 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -23,27 +23,6 @@ pub enum Event { /// A touch event Touch(touch::Event), - - /// A platform specific event - PlatformSpecific(PlatformSpecific), -} - -/// A platform specific event -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PlatformSpecific { - /// A MacOS specific event - MacOS(MacOS), -} - -/// Describes an event specific to MacOS -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacOS { - /// Triggered when the app receives an URL from the system - /// - /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ - /// - /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 - ReceivedUrl(String), } /// The status of an [`Event`] after being processed. diff --git a/core/src/program.rs b/core/src/program.rs new file mode 100644 index 00000000..16fd7e8f --- /dev/null +++ b/core/src/program.rs @@ -0,0 +1 @@ +use crate::window; diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 800a188b..3ab19252 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,4 +1,4 @@ -use iced::event::{self, Event}; +use iced::event; use iced::widget::{center, text}; use iced::{Element, Subscription}; @@ -15,27 +15,20 @@ struct App { #[derive(Debug, Clone)] enum Message { - EventOccurred(Event), + UrlReceived(String), } impl App { fn update(&mut self, message: Message) { match message { - Message::EventOccurred(event) => { - if let Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - ) = event - { - self.url = Some(url); - } + Message::UrlReceived(url) => { + self.url = Some(url); } } } fn subscription(&self) -> Subscription { - event::listen().map(Message::EventOccurred) + event::listen_url().map(Message::UrlReceived) } fn view(&self) -> Element { diff --git a/futures/src/event.rs b/futures/src/event.rs index 4f3342ca..72ea78ad 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -32,11 +32,17 @@ where #[derive(Hash)] struct EventsWith; - subscription::filter_map((EventsWith, f), move |event, status, window| { - match event { - Event::Window(window::Event::RedrawRequested(_)) => None, - _ => f(event, status, window), + subscription::filter_map((EventsWith, f), move |event| match event { + subscription::Event::Interaction { + event: Event::Window(window::Event::RedrawRequested(_)), + .. } + | subscription::Event::PlatformSpecific(_) => None, + subscription::Event::Interaction { + window, + event, + status, + } => f(event, status, window), }) } @@ -54,5 +60,32 @@ where #[derive(Hash)] struct RawEvents; - subscription::filter_map((RawEvents, f), f) + subscription::filter_map((RawEvents, f), move |event| match event { + subscription::Event::Interaction { + window, + event, + status, + } => f(event, status, window), + subscription::Event::PlatformSpecific(_) => None, + }) +} + +/// Creates a [`Subscription`] that notifies of custom application URL +/// received from the system. +/// +/// _**Note:** Currently, it only triggers on macOS and the executable needs to be properly [bundled]!_ +/// +/// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 +pub fn listen_url() -> Subscription { + #[derive(Hash)] + struct ListenUrl; + + subscription::filter_map(ListenUrl, move |event| match event { + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), + ) => Some(url), + _ => None, + }) } diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index 43ed7742..f0d7d757 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -1,5 +1,6 @@ //! Listen to keyboard events. use crate::core; +use crate::core::event; use crate::core::keyboard::{Event, Key, Modifiers}; use crate::subscription::{self, Subscription}; use crate::MaybeSend; @@ -18,16 +19,14 @@ where #[derive(Hash)] struct OnKeyPress; - subscription::filter_map((OnKeyPress, f), move |event, status, _window| { - match (event, status) { - ( - core::Event::Keyboard(Event::KeyPressed { - key, modifiers, .. - }), - core::event::Status::Ignored, - ) => f(key, modifiers), - _ => None, - } + subscription::filter_map((OnKeyPress, f), move |event| match event { + subscription::Event::Interaction { + event: + core::Event::Keyboard(Event::KeyPressed { key, modifiers, .. }), + status: event::Status::Ignored, + .. + } => f(key, modifiers), + _ => None, }) } @@ -45,18 +44,13 @@ where #[derive(Hash)] struct OnKeyRelease; - subscription::filter_map( - (OnKeyRelease, f), - move |event, status, _window| match (event, status) { - ( - core::Event::Keyboard(Event::KeyReleased { - key, - modifiers, - .. - }), - core::event::Status::Ignored, - ) => f(key, modifiers), - _ => None, - }, - ) + subscription::filter_map((OnKeyRelease, f), move |event| match event { + subscription::Event::Interaction { + event: + core::Event::Keyboard(Event::KeyReleased { key, modifiers, .. }), + status: event::Status::Ignored, + .. + } => f(key, modifiers), + _ => None, + }) } diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index ae55f814..157e2c67 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,6 +1,4 @@ //! Run commands and keep track of subscriptions. -use crate::core::event::{self, Event}; -use crate::core::window; use crate::subscription; use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; @@ -128,12 +126,7 @@ where /// See [`Tracker::broadcast`] to learn more. /// /// [`Tracker::broadcast`]: subscription::Tracker::broadcast - pub fn broadcast( - &mut self, - event: Event, - status: event::Status, - window: window::Id, - ) { - self.subscriptions.broadcast(event, status, window); + pub fn broadcast(&mut self, event: subscription::Event) { + self.subscriptions.broadcast(event); } } diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 79cea6ed..316fc44d 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -3,7 +3,7 @@ mod tracker; pub use tracker::Tracker; -use crate::core::event::{self, Event}; +use crate::core::event; use crate::core::window; use crate::futures::{Future, Stream}; use crate::{BoxStream, MaybeSend}; @@ -13,10 +13,48 @@ use futures::never::Never; use std::any::TypeId; use std::hash::Hash; +/// A subscription event. +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// A user interacted with a user interface in a window. + Interaction { + /// The window holding the interface of the interaction. + window: window::Id, + /// The [`Event`] describing the interaction. + /// + /// [`Event`]: event::Event + event: event::Event, + + /// The [`event::Status`] of the interaction. + status: event::Status, + }, + + /// A platform specific event. + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlatformSpecific { + /// A MacOS specific event + MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacOS { + /// Triggered when the app receives an URL from the system + /// + /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ + /// + /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 + ReceivedUrl(String), +} + /// A stream of runtime events. /// /// It is the input of a [`Subscription`]. -pub type EventStream = BoxStream<(Event, event::Status, window::Id)>; +pub type EventStream = BoxStream; /// The hasher used for identifying subscriptions. pub type Hasher = rustc_hash::FxHasher; @@ -290,9 +328,7 @@ where pub(crate) fn filter_map(id: I, f: F) -> Subscription where I: Hash + 'static, - F: Fn(Event, event::Status, window::Id) -> Option - + MaybeSend - + 'static, + F: Fn(Event) -> Option + MaybeSend + 'static, Message: 'static + MaybeSend, { Subscription::from_recipe(Runner { @@ -301,9 +337,7 @@ where use futures::future; use futures::stream::StreamExt; - events.filter_map(move |(event, status, window)| { - future::ready(f(event, status, window)) - }) + events.filter_map(move |event| future::ready(f(event))) }, }) } diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index 086b0f09..f17e3ea3 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,6 +1,4 @@ -use crate::core::event::{self, Event}; -use crate::core::window; -use crate::subscription::{Hasher, Recipe}; +use crate::subscription::{Event, Hasher, Recipe}; use crate::{BoxFuture, MaybeSend}; use futures::channel::mpsc; @@ -24,9 +22,7 @@ pub struct Tracker { #[derive(Debug)] pub struct Execution { _cancel: futures::channel::oneshot::Sender<()>, - listener: Option< - futures::channel::mpsc::Sender<(Event, event::Status, window::Id)>, - >, + listener: Option>, } impl Tracker { @@ -142,19 +138,12 @@ impl Tracker { /// currently open. /// /// [`Recipe::stream`]: crate::subscription::Recipe::stream - pub fn broadcast( - &mut self, - event: Event, - status: event::Status, - window: window::Id, - ) { + pub fn broadcast(&mut self, event: Event) { self.subscriptions .values_mut() .filter_map(|connection| connection.listener.as_mut()) .for_each(|listener| { - if let Err(error) = - listener.try_send((event.clone(), status, window)) - { + if let Err(error) = listener.try_send(event.clone()) { log::warn!( "Error sending event to subscription: {error:?}" ); diff --git a/src/lib.rs b/src/lib.rs index 50ee7ecc..317d25a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -236,8 +236,10 @@ pub mod font { pub mod event { //! Handle events of a user interface. - pub use crate::core::event::{Event, MacOS, PlatformSpecific, Status}; - pub use iced_futures::event::{listen, listen_raw, listen_with}; + pub use crate::core::event::{Event, Status}; + pub use iced_futures::event::{ + listen, listen_raw, listen_url, listen_with, + }; } pub mod keyboard { diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index be09f163..73cef087 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -172,7 +172,7 @@ where core::Event::Keyboard(keyboard_event) => { Some(Event::Keyboard(keyboard_event)) } - _ => None, + core::Event::Window(_) => None, }; if let Some(canvas_event) = canvas_event { diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 473cfd0d..3c81f8ed 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -110,7 +110,7 @@ where core::Event::Window(window::Event::RedrawRequested(instant)) => { Some(Event::RedrawRequested(instant)) } - _ => None, + core::Event::Window(_) => None, }; if let Some(custom_shader_event) = custom_shader_event { diff --git a/winit/src/application.rs b/winit/src/application.rs index ed8715d8..d93ea42e 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -12,7 +12,8 @@ use crate::core::widget::operation; use crate::core::window; use crate::core::{Color, Event, Point, Size, Theme}; use crate::futures::futures; -use crate::futures::{Executor, Runtime, Subscription}; +use crate::futures::subscription::{self, Subscription}; +use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; @@ -574,12 +575,10 @@ async fn run_instance( event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), )) => { - use crate::core::event; - - events.push(Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), + runtime.broadcast(subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), )); } event::Event::UserEvent(message) => { @@ -650,11 +649,11 @@ async fn run_instance( _ => ControlFlow::Wait, }); - runtime.broadcast( - redraw_event, - core::event::Status::Ignored, - window::Id::MAIN, - ); + runtime.broadcast(subscription::Event::Interaction { + window: window::Id::MAIN, + event: redraw_event, + status: core::event::Status::Ignored, + }); debug.draw_started(); let new_mouse_interaction = user_interface.draw( @@ -744,7 +743,11 @@ async fn run_instance( for (event, status) in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event, status, window::Id::MAIN); + runtime.broadcast(subscription::Event::Interaction { + window: window::Id::MAIN, + event, + status, + }); } if !messages.is_empty() diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 13839828..2eaf9241 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -16,7 +16,8 @@ use crate::futures::futures::channel::oneshot; use crate::futures::futures::executor; use crate::futures::futures::task; use crate::futures::futures::{Future, StreamExt}; -use crate::futures::{Executor, Runtime, Subscription}; +use crate::futures::subscription::{self, Subscription}; +use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::{compositor, Compositor}; use crate::multi_window::window_manager::WindowManager; @@ -585,16 +586,13 @@ async fn run_instance( event::MacOS::ReceivedUrl(url), ), ) => { - use crate::core::event; - - events.push(( - window::Id::MAIN, - event::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), + runtime.broadcast( + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), ), ), - )); + ); } event::Event::UserEvent(message) => { messages.push(message); @@ -655,11 +653,11 @@ async fn run_instance( window.mouse_interaction = new_mouse_interaction; } - runtime.broadcast( - redraw_event.clone(), - core::event::Status::Ignored, - id, - ); + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); let _ = control_sender.start_send(Control::ChangeFlow( match ui_state { @@ -866,15 +864,23 @@ async fn run_instance( .into_iter() .zip(statuses.into_iter()) { - runtime.broadcast(event, status, id); + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status, + }, + ); } } for (id, event) in events.drain(..) { runtime.broadcast( - event, - core::event::Status::Ignored, - id, + subscription::Event::Interaction { + window: id, + event, + status: core::event::Status::Ignored, + }, ); } From 6a03b8489b0a92535963b123b4ab91e485f6689b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 11 Jun 2024 20:03:28 +0200 Subject: [PATCH 032/657] Remove `tracing` leftovers in `iced_wgpu` --- wgpu/src/quad/gradient.rs | 3 --- wgpu/src/quad/solid.rs | 3 --- 2 files changed, 6 deletions(-) diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 5b32c52a..13dc10f8 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -188,9 +188,6 @@ impl Pipeline { layer: &'a Layer, range: Range, ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered(); - #[cfg(not(target_arch = "wasm32"))] { render_pass.set_pipeline(&self.pipeline); diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 1cead367..45039a2d 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -144,9 +144,6 @@ impl Pipeline { layer: &'a Layer, range: Range, ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Quad::Solid", "DRAW").entered(); - render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, constants, &[]); render_pass.set_vertex_buffer(0, layer.instances.slice(..)); From df85d85a6fa1e50dc0e21b0b0580eb5f797a80fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 11 Jun 2024 20:07:06 +0200 Subject: [PATCH 033/657] Allow `dead_code` in `widget::lazy::cache` --- widget/src/lazy/cache.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/widget/src/lazy/cache.rs b/widget/src/lazy/cache.rs index f922fd19..b341c234 100644 --- a/widget/src/lazy/cache.rs +++ b/widget/src/lazy/cache.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use crate::core::overlay; use crate::core::Element; From 6ea7846d88915f8d820c5126d7757f1346234522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 11 Jun 2024 20:11:55 +0200 Subject: [PATCH 034/657] Remove `core::program` module leftover --- core/src/program.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 core/src/program.rs diff --git a/core/src/program.rs b/core/src/program.rs deleted file mode 100644 index 16fd7e8f..00000000 --- a/core/src/program.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::window; From 06c80c5bcedd8dba187af9d187ba1ae40fa4dafa Mon Sep 17 00:00:00 2001 From: Andrew-Schwartz Date: Wed, 12 Jun 2024 10:54:50 -0400 Subject: [PATCH 035/657] Add FromIterator for Row and Column --- widget/src/column.rs | 42 ++++++++++++++++++++++++------------------ widget/src/row.rs | 6 ++++++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/widget/src/column.rs b/widget/src/column.rs index df7829b3..708c0806 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,14 +1,14 @@ //! Distribute content vertically. +use crate::core::{ + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Size, Vector, Widget, +}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; -use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Size, Vector, Widget, -}; /// A container that distributes its contents vertically. #[allow(missing_debug_implementations)] @@ -25,8 +25,8 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> } impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> -where - Renderer: crate::core::Renderer, + where + Renderer: crate::core::Renderer, { /// Creates an empty [`Column`]. pub fn new() -> Self { @@ -40,7 +40,7 @@ where /// Creates a [`Column`] with the given elements. pub fn with_children( - children: impl IntoIterator>, + children: impl IntoIterator>, ) -> Self { let iterator = children.into_iter(); @@ -146,25 +146,31 @@ where /// Extends the [`Column`] with the given children. pub fn extend( self, - children: impl IntoIterator>, + children: impl IntoIterator>, ) -> Self { children.into_iter().fold(self, Self::push) } } impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> -where - Renderer: crate::core::Renderer, + where + Renderer: crate::core::Renderer, { fn default() -> Self { Self::new() } } +impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Column<'a, Message, Theme, Renderer> { + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + impl<'a, Message, Theme, Renderer> Widget - for Column<'a, Message, Theme, Renderer> -where - Renderer: crate::core::Renderer, +for Column<'a, Message, Theme, Renderer> + where + Renderer: crate::core::Renderer, { fn children(&self) -> Vec { self.children.iter().map(Tree::new).collect() @@ -326,11 +332,11 @@ where } impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Theme: 'a, - Renderer: crate::core::Renderer + 'a, +for Element<'a, Message, Theme, Renderer> + where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, { fn from(column: Column<'a, Message, Theme, Renderer>) -> Self { Self::new(column) diff --git a/widget/src/row.rs b/widget/src/row.rs index fa352171..5a6b368a 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -152,6 +152,12 @@ where } } +impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Row<'a, Message, Theme, Renderer> { + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + impl<'a, Message, Theme, Renderer> Widget for Row<'a, Message, Theme, Renderer> where From 1355f8d296fea59bafac600982fafb23d8aac602 Mon Sep 17 00:00:00 2001 From: Andrew-Schwartz Date: Wed, 12 Jun 2024 13:00:02 -0400 Subject: [PATCH 036/657] Revert "Add FromIterator for Row and Column" This reverts commit 06c80c5bcedd8dba187af9d187ba1ae40fa4dafa. --- widget/src/column.rs | 42 ++++++++++++++++++------------------------ widget/src/row.rs | 6 ------ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/widget/src/column.rs b/widget/src/column.rs index 708c0806..df7829b3 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,14 +1,14 @@ //! Distribute content vertically. -use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Size, Vector, Widget, -}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; +use crate::core::{ + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Size, Vector, Widget, +}; /// A container that distributes its contents vertically. #[allow(missing_debug_implementations)] @@ -25,8 +25,8 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> } impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> - where - Renderer: crate::core::Renderer, +where + Renderer: crate::core::Renderer, { /// Creates an empty [`Column`]. pub fn new() -> Self { @@ -40,7 +40,7 @@ impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> /// Creates a [`Column`] with the given elements. pub fn with_children( - children: impl IntoIterator>, + children: impl IntoIterator>, ) -> Self { let iterator = children.into_iter(); @@ -146,31 +146,25 @@ impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> /// Extends the [`Column`] with the given children. pub fn extend( self, - children: impl IntoIterator>, + children: impl IntoIterator>, ) -> Self { children.into_iter().fold(self, Self::push) } } impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> - where - Renderer: crate::core::Renderer, +where + Renderer: crate::core::Renderer, { fn default() -> Self { Self::new() } } -impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Column<'a, Message, Theme, Renderer> { - fn from_iter>>(iter: T) -> Self { - Self::with_children(iter) - } -} - impl<'a, Message, Theme, Renderer> Widget -for Column<'a, Message, Theme, Renderer> - where - Renderer: crate::core::Renderer, + for Column<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, { fn children(&self) -> Vec { self.children.iter().map(Tree::new).collect() @@ -332,11 +326,11 @@ for Column<'a, Message, Theme, Renderer> } impl<'a, Message, Theme, Renderer> From> -for Element<'a, Message, Theme, Renderer> - where - Message: 'a, - Theme: 'a, - Renderer: crate::core::Renderer + 'a, + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, { fn from(column: Column<'a, Message, Theme, Renderer>) -> Self { Self::new(column) diff --git a/widget/src/row.rs b/widget/src/row.rs index 5a6b368a..fa352171 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -152,12 +152,6 @@ where } } -impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Row<'a, Message, Theme, Renderer> { - fn from_iter>>(iter: T) -> Self { - Self::with_children(iter) - } -} - impl<'a, Message, Theme, Renderer> Widget for Row<'a, Message, Theme, Renderer> where From 65eb06c53743c2aa924bd90ee7b7c62c3541fa08 Mon Sep 17 00:00:00 2001 From: Andrew-Schwartz Date: Wed, 12 Jun 2024 13:01:04 -0400 Subject: [PATCH 037/657] Add FromIterator for Row and Column --- widget/src/column.rs | 6 ++++++ widget/src/row.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/widget/src/column.rs b/widget/src/column.rs index df7829b3..fbdb02d4 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -161,6 +161,12 @@ where } } +impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Column<'a, Message, Theme, Renderer> { + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + impl<'a, Message, Theme, Renderer> Widget for Column<'a, Message, Theme, Renderer> where diff --git a/widget/src/row.rs b/widget/src/row.rs index fa352171..5a6b368a 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -152,6 +152,12 @@ where } } +impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Row<'a, Message, Theme, Renderer> { + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + impl<'a, Message, Theme, Renderer> Widget for Row<'a, Message, Theme, Renderer> where From 620c3d3222fa7ea17fd8d48af656f644bf409aa2 Mon Sep 17 00:00:00 2001 From: Andrew-Schwartz Date: Thu, 13 Jun 2024 14:46:56 -0400 Subject: [PATCH 038/657] format --- widget/src/column.rs | 11 +++++++++-- widget/src/row.rs | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/widget/src/column.rs b/widget/src/column.rs index fbdb02d4..8b97e691 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -161,8 +161,15 @@ where } } -impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Column<'a, Message, Theme, Renderer> { - fn from_iter>>(iter: T) -> Self { +impl<'a, Message, Theme, Renderer: crate::core::Renderer> + FromIterator> + for Column<'a, Message, Theme, Renderer> +{ + fn from_iter< + T: IntoIterator>, + >( + iter: T, + ) -> Self { Self::with_children(iter) } } diff --git a/widget/src/row.rs b/widget/src/row.rs index 5a6b368a..271e8a50 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -152,8 +152,15 @@ where } } -impl<'a, Message, Theme, Renderer: crate::core::Renderer> FromIterator> for Row<'a, Message, Theme, Renderer> { - fn from_iter>>(iter: T) -> Self { +impl<'a, Message, Theme, Renderer: crate::core::Renderer> + FromIterator> + for Row<'a, Message, Theme, Renderer> +{ + fn from_iter< + T: IntoIterator>, + >( + iter: T, + ) -> Self { Self::with_children(iter) } } From a25b1af45690bdd8e1cbb20ee3a5b1c4342de455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 01:47:39 +0200 Subject: [PATCH 039/657] Replace `Command` with a new `Task` API with chain support --- core/src/element.rs | 61 +-- core/src/lib.rs | 2 + {futures => core}/src/maybe.rs | 0 core/src/overlay.rs | 2 +- core/src/overlay/element.rs | 60 +-- core/src/overlay/group.rs | 2 +- core/src/widget.rs | 2 +- core/src/widget/operation.rs | 12 +- examples/editor/src/main.rs | 26 +- examples/events/src/main.rs | 10 +- examples/exit/src/main.rs | 6 +- examples/game_of_life/src/main.rs | 8 +- examples/integration/src/controls.rs | 6 +- examples/loading_spinners/src/easing.rs | 5 +- examples/modal/src/main.rs | 18 +- examples/multi_window/src/main.rs | 20 +- examples/pokedex/src/main.rs | 14 +- examples/screenshot/src/main.rs | 14 +- examples/scrollable/src/main.rs | 12 +- examples/system_information/src/main.rs | 9 +- examples/toast/src/main.rs | 22 +- examples/todos/src/main.rs | 2 +- examples/visible_bounds/src/main.rs | 14 +- examples/websocket/src/main.rs | 22 +- futures/src/event.rs | 2 +- futures/src/executor.rs | 2 +- futures/src/keyboard.rs | 2 +- futures/src/lib.rs | 2 - futures/src/runtime.rs | 3 +- futures/src/subscription.rs | 3 +- futures/src/subscription/tracker.rs | 3 +- graphics/src/compositor.rs | 3 +- runtime/src/clipboard.rs | 98 ++-- runtime/src/command.rs | 147 ------ runtime/src/command/action.rs | 100 ---- runtime/src/font.rs | 12 +- runtime/src/lib.rs | 80 +++- runtime/src/multi_window/program.rs | 8 +- runtime/src/multi_window/state.rs | 18 +- runtime/src/overlay/nested.rs | 4 +- runtime/src/program.rs | 6 +- runtime/src/program/state.rs | 33 +- runtime/src/system.rs | 41 +- runtime/src/system/action.rs | 39 -- runtime/src/system/information.rs | 29 -- runtime/src/task.rs | 214 +++++++++ runtime/src/user_interface.rs | 2 +- runtime/src/window.rs | 295 ++++++++---- runtime/src/window/action.rs | 230 ---------- src/application.rs | 26 +- src/lib.rs | 7 +- src/multi_window.rs | 26 +- src/program.rs | 60 +-- widget/src/button.rs | 2 +- widget/src/column.rs | 2 +- widget/src/container.rs | 10 +- widget/src/helpers.rs | 20 +- widget/src/keyed/column.rs | 2 +- widget/src/lazy.rs | 2 +- widget/src/lazy/component.rs | 61 +-- widget/src/lazy/responsive.rs | 2 +- widget/src/mouse_area.rs | 2 +- widget/src/pane_grid.rs | 2 +- widget/src/pane_grid/content.rs | 2 +- widget/src/pane_grid/title_bar.rs | 2 +- widget/src/row.rs | 2 +- widget/src/scrollable.rs | 24 +- widget/src/stack.rs | 2 +- widget/src/text_input.rs | 43 +- widget/src/themer.rs | 4 +- winit/src/application.rs | 470 +++++++++---------- winit/src/multi_window.rs | 581 +++++++++++------------- winit/src/proxy.rs | 27 +- winit/src/system.rs | 12 +- 74 files changed, 1351 insertions(+), 1767 deletions(-) rename {futures => core}/src/maybe.rs (100%) delete mode 100644 runtime/src/command.rs delete mode 100644 runtime/src/command/action.rs delete mode 100644 runtime/src/system/action.rs delete mode 100644 runtime/src/system/information.rs create mode 100644 runtime/src/task.rs delete mode 100644 runtime/src/window/action.rs diff --git a/core/src/element.rs b/core/src/element.rs index 7d918a2e..385d8295 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -10,7 +10,6 @@ use crate::{ Widget, }; -use std::any::Any; use std::borrow::Borrow; /// A generic [`Widget`]. @@ -305,63 +304,9 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - bounds: Rectangle, - translation: Vector, - ) { - self.operation.scrollable(state, id, bounds, translation); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.widget.operate( - tree, - layout, - renderer, - &mut MapOperation { operation }, - ); + self.widget.operate(tree, layout, renderer, operation); } fn on_event( @@ -495,7 +440,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { self.element .widget diff --git a/core/src/lib.rs b/core/src/lib.rs index 32156441..db67219c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -35,6 +35,7 @@ mod color; mod content_fit; mod element; mod length; +mod maybe; mod padding; mod pixels; mod point; @@ -59,6 +60,7 @@ pub use font::Font; pub use gradient::Gradient; pub use layout::Layout; pub use length::Length; +pub use maybe::{MaybeSend, MaybeSync}; pub use overlay::Overlay; pub use padding::Padding; pub use pixels::Pixels; diff --git a/futures/src/maybe.rs b/core/src/maybe.rs similarity index 100% rename from futures/src/maybe.rs rename to core/src/maybe.rs diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 3a57fe16..16f867da 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -41,7 +41,7 @@ where &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn widget::Operation<()>, ) { } diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 695b88b3..61e75e8a 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -5,9 +5,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::widget; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; - -use std::any::Any; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// A generic [`Overlay`]. #[allow(missing_debug_implementations)] @@ -94,7 +92,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { self.overlay.operate(layout, renderer, operation); } @@ -146,59 +144,9 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - bounds: Rectangle, - translation: Vector, - ) { - self.operation.scrollable(state, id, bounds, translation); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.content - .operate(layout, renderer, &mut MapOperation { operation }); + self.content.operate(layout, renderer, operation); } fn on_event( diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 7e4bebd0..cd12eac9 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -132,7 +132,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/widget.rs b/core/src/widget.rs index b02e3a4f..0d12deba 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -105,7 +105,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation, + _operation: &mut dyn Operation<()>, ) { } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index b91cf9ac..1fa924a4 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -8,15 +8,15 @@ pub use scrollable::Scrollable; pub use text_input::TextInput; use crate::widget::Id; -use crate::{Rectangle, Vector}; +use crate::{MaybeSend, Rectangle, Vector}; use std::any::Any; use std::fmt; -use std::rc::Rc; +use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. -pub trait Operation { +pub trait Operation: MaybeSend { /// Operates on a widget that contains other widgets. /// /// The `operate_on_children` function can be called to return control to @@ -81,7 +81,7 @@ where /// Maps the output of an [`Operation`] using the given function. pub fn map( operation: Box>, - f: impl Fn(A) -> B + 'static, + f: impl Fn(A) -> B + Send + Sync + 'static, ) -> impl Operation where A: 'static, @@ -90,7 +90,7 @@ where #[allow(missing_debug_implementations)] struct Map { operation: Box>, - f: Rc B>, + f: Arc B + Send + Sync>, } impl Operation for Map @@ -197,7 +197,7 @@ where Map { operation, - f: Rc::new(f), + f: Arc::new(f), } } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index c20a7d67..ec65e2fa 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Alignment, Command, Element, Font, Length, Subscription, Theme}; +use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme}; use std::ffi; use std::io; @@ -51,26 +51,26 @@ impl Editor { } } - fn load() -> Command { - Command::perform( + fn load() -> Task { + Task::perform( load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))), Message::FileOpened, ) } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::ActionPerformed(action) => { self.is_dirty = self.is_dirty || action.is_edit(); self.content.perform(action); - Command::none() + Task::none() } Message::ThemeSelected(theme) => { self.theme = theme; - Command::none() + Task::none() } Message::NewFile => { if !self.is_loading { @@ -78,15 +78,15 @@ impl Editor { self.content = text_editor::Content::new(); } - Command::none() + Task::none() } Message::OpenFile => { if self.is_loading { - Command::none() + Task::none() } else { self.is_loading = true; - Command::perform(open_file(), Message::FileOpened) + Task::perform(open_file(), Message::FileOpened) } } Message::FileOpened(result) => { @@ -98,15 +98,15 @@ impl Editor { self.content = text_editor::Content::with_text(&contents); } - Command::none() + Task::none() } Message::SaveFile => { if self.is_loading { - Command::none() + Task::none() } else { self.is_loading = true; - Command::perform( + Task::perform( save_file(self.file.clone(), self.content.text()), Message::FileSaved, ) @@ -120,7 +120,7 @@ impl Editor { self.is_dirty = false; } - Command::none() + Task::none() } } } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index bacd8e6e..504ed5d8 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -2,7 +2,7 @@ use iced::alignment; use iced::event::{self, Event}; use iced::widget::{button, center, checkbox, text, Column}; use iced::window; -use iced::{Alignment, Command, Element, Length, Subscription}; +use iced::{Alignment, Element, Length, Subscription, Task}; pub fn main() -> iced::Result { iced::program("Events - Iced", Events::update, Events::view) @@ -25,7 +25,7 @@ enum Message { } impl Events { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::EventOccurred(event) if self.enabled => { self.last.push(event); @@ -34,19 +34,19 @@ impl Events { let _ = self.last.remove(0); } - Command::none() + Task::none() } Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { window::close(window::Id::MAIN) } else { - Command::none() + Task::none() } } Message::Toggled(enabled) => { self.enabled = enabled; - Command::none() + Task::none() } Message::Exit => window::close(window::Id::MAIN), } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 2de97e20..8ba180a5 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{button, center, column}; use iced::window; -use iced::{Alignment, Command, Element}; +use iced::{Alignment, Element, Task}; pub fn main() -> iced::Result { iced::program("Exit - Iced", Exit::update, Exit::view).run() @@ -18,13 +18,13 @@ enum Message { } impl Exit { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::Confirm => window::close(window::Id::MAIN), Message::Exit => { self.show_confirm = true; - Command::none() + Task::none() } } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8f1f7a54..7e6d461d 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -9,7 +9,7 @@ use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, }; -use iced::{Alignment, Command, Element, Length, Subscription, Theme}; +use iced::{Alignment, Element, Length, Subscription, Task, Theme}; use std::time::Duration; pub fn main() -> iced::Result { @@ -56,7 +56,7 @@ impl GameOfLife { } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::Grid(message, version) => { if version == self.version { @@ -75,7 +75,7 @@ impl GameOfLife { let version = self.version; - return Command::perform(task, move |message| { + return Task::perform(task, move |message| { Message::Grid(message, version) }); } @@ -103,7 +103,7 @@ impl GameOfLife { } } - Command::none() + Task::none() } fn subscription(&self) -> Subscription { diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 1958b2f3..d0654996 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -2,7 +2,7 @@ use iced_wgpu::Renderer; use iced_widget::{column, container, row, slider, text, text_input}; use iced_winit::core::alignment; use iced_winit::core::{Color, Element, Length, Theme}; -use iced_winit::runtime::{Command, Program}; +use iced_winit::runtime::{Program, Task}; pub struct Controls { background_color: Color, @@ -33,7 +33,7 @@ impl Program for Controls { type Message = Message; type Renderer = Renderer; - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::BackgroundColorChanged(color) => { self.background_color = color; @@ -43,7 +43,7 @@ impl Program for Controls { } } - Command::none() + Task::none() } fn view(&self) -> Element { diff --git a/examples/loading_spinners/src/easing.rs b/examples/loading_spinners/src/easing.rs index 665b3329..45089ef6 100644 --- a/examples/loading_spinners/src/easing.rs +++ b/examples/loading_spinners/src/easing.rs @@ -119,10 +119,7 @@ impl Builder { fn point(p: impl Into) -> lyon_algorithms::geom::Point { let p: Point = p.into(); - lyon_algorithms::geom::point( - p.x.min(1.0).max(0.0), - p.y.min(1.0).max(0.0), - ) + lyon_algorithms::geom::point(p.x.clamp(0.0, 1.0), p.y.clamp(0.0, 1.0)) } } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index a012c310..d185cf3b 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ self, button, center, column, container, horizontal_space, mouse_area, opaque, pick_list, row, stack, text, text_input, }; -use iced::{Alignment, Color, Command, Element, Length, Subscription}; +use iced::{Alignment, Color, Element, Length, Subscription, Task}; use std::fmt; @@ -39,7 +39,7 @@ impl App { event::listen().map(Message::Event) } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::ShowModal => { self.show_modal = true; @@ -47,26 +47,26 @@ impl App { } Message::HideModal => { self.hide_modal(); - Command::none() + Task::none() } Message::Email(email) => { self.email = email; - Command::none() + Task::none() } Message::Password(password) => { self.password = password; - Command::none() + Task::none() } Message::Plan(plan) => { self.plan = plan; - Command::none() + Task::none() } Message::Submit => { if !self.email.is_empty() && !self.password.is_empty() { self.hide_modal(); } - Command::none() + Task::none() } Message::Event(event) => match event { Event::Keyboard(keyboard::Event::KeyPressed { @@ -85,9 +85,9 @@ impl App { .. }) => { self.hide_modal(); - Command::none() + Task::none() } - _ => Command::none(), + _ => Task::none(), }, } } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index eb74c94a..e15f8759 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, + Alignment, Element, Length, Point, Settings, Subscription, Task, Theme, Vector, }; @@ -48,13 +48,13 @@ impl multi_window::Application for Example { type Theme = Theme; type Flags = (); - fn new(_flags: ()) -> (Self, Command) { + fn new(_flags: ()) -> (Self, Task) { ( Example { windows: HashMap::from([(window::Id::MAIN, Window::new(1))]), next_window_pos: window::Position::Default, }, - Command::none(), + Task::none(), ) } @@ -65,14 +65,14 @@ impl multi_window::Application for Example { .unwrap_or("Example".to_string()) } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::ScaleInputChanged(id, scale) => { let window = self.windows.get_mut(&id).expect("Window not found!"); window.scale_input = scale; - Command::none() + Task::none() } Message::ScaleChanged(id, scale) => { let window = @@ -83,7 +83,7 @@ impl multi_window::Application for Example { .unwrap_or(window.current_scale) .clamp(0.5, 5.0); - Command::none() + Task::none() } Message::TitleChanged(id, title) => { let window = @@ -91,12 +91,12 @@ impl multi_window::Application for Example { window.title = title; - Command::none() + Task::none() } Message::CloseWindow(id) => window::close(id), Message::WindowClosed(id) => { self.windows.remove(&id); - Command::none() + Task::none() } Message::WindowOpened(id, position) => { if let Some(position) = position { @@ -108,13 +108,13 @@ impl multi_window::Application for Example { if let Some(window) = self.windows.get(&id) { text_input::focus(window.input_id.clone()) } else { - Command::none() + Task::none() } } Message::NewWindow => { let count = self.windows.len() + 1; - let (id, spawn_window) = window::spawn(window::Settings { + let (id, spawn_window) = window::open(window::Settings { position: self.next_window_pos, exit_on_close_request: count % 2 == 0, ..Default::default() diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index cffa3727..e62ed70b 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,6 @@ use iced::futures; use iced::widget::{self, center, column, image, row, text}; -use iced::{Alignment, Command, Element, Length}; +use iced::{Alignment, Element, Length, Task}; pub fn main() -> iced::Result { iced::program(Pokedex::title, Pokedex::update, Pokedex::view) @@ -25,8 +25,8 @@ enum Message { } impl Pokedex { - fn search() -> Command { - Command::perform(Pokemon::search(), Message::PokemonFound) + fn search() -> Task { + Task::perform(Pokemon::search(), Message::PokemonFound) } fn title(&self) -> String { @@ -39,20 +39,20 @@ impl Pokedex { format!("{subtitle} - Pokédex") } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::PokemonFound(Ok(pokemon)) => { *self = Pokedex::Loaded { pokemon }; - Command::none() + Task::none() } Message::PokemonFound(Err(_error)) => { *self = Pokedex::Errored; - Command::none() + Task::none() } Message::Search => match self { - Pokedex::Loading => Command::none(), + Pokedex::Loading => Task::none(), _ => { *self = Pokedex::Loading; diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index fb19e556..9b9162d0 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; use iced::{ - Alignment, Command, ContentFit, Element, Length, Rectangle, Subscription, + Alignment, ContentFit, Element, Length, Rectangle, Subscription, Task, }; use ::image as img; @@ -44,13 +44,11 @@ enum Message { } impl Example { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::Screenshot => { - return iced::window::screenshot( - window::Id::MAIN, - Message::ScreenshotData, - ); + return iced::window::screenshot(window::Id::MAIN) + .map(Message::ScreenshotData); } Message::ScreenshotData(screenshot) => { self.screenshot = Some(screenshot); @@ -59,7 +57,7 @@ impl Example { if let Some(screenshot) = &self.screenshot { self.png_saving = true; - return Command::perform( + return Task::perform( save_to_png(screenshot.clone()), Message::PngSaved, ); @@ -103,7 +101,7 @@ impl Example { } } - Command::none() + Task::none() } fn view(&self) -> Element<'_, Message> { diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index bbb6497f..a0dcf82c 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, Scrollable, }; -use iced::{Alignment, Border, Color, Command, Element, Length, Theme}; +use iced::{Alignment, Border, Color, Element, Length, Task, Theme}; use once_cell::sync::Lazy; @@ -59,7 +59,7 @@ impl ScrollableDemo { } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::SwitchDirection(direction) => { self.current_scroll_offset = scrollable::RelativeOffset::START; @@ -82,17 +82,17 @@ impl ScrollableDemo { Message::ScrollbarWidthChanged(width) => { self.scrollbar_width = width; - Command::none() + Task::none() } Message::ScrollbarMarginChanged(margin) => { self.scrollbar_margin = margin; - Command::none() + Task::none() } Message::ScrollerWidthChanged(width) => { self.scroller_width = width; - Command::none() + Task::none() } Message::ScrollToBeginning => { self.current_scroll_offset = scrollable::RelativeOffset::START; @@ -113,7 +113,7 @@ impl ScrollableDemo { Message::Scrolled(viewport) => { self.current_scroll_offset = viewport.relative_offset(); - Command::none() + Task::none() } } } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index 8ce12e1c..e2808edd 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{button, center, column, text}; -use iced::{system, Command, Element}; +use iced::{system, Element, Task}; pub fn main() -> iced::Result { iced::program("System Information - Iced", Example::update, Example::view) @@ -24,19 +24,20 @@ enum Message { } impl Example { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::Refresh => { *self = Self::Loading; - return system::fetch_information(Message::InformationReceived); + return system::fetch_information() + .map(Message::InformationReceived); } Message::InformationReceived(information) => { *self = Self::Loaded { information }; } } - Command::none() + Task::none() } fn view(&self) -> Element { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 700b6b10..aee2479e 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -4,7 +4,7 @@ use iced::keyboard::key; use iced::widget::{ self, button, center, column, pick_list, row, slider, text, text_input, }; -use iced::{Alignment, Command, Element, Length, Subscription}; +use iced::{Alignment, Element, Length, Subscription, Task}; use toast::{Status, Toast}; @@ -49,7 +49,7 @@ impl App { event::listen().map(Message::Event) } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::Add => { if !self.editing.title.is_empty() @@ -57,27 +57,27 @@ impl App { { self.toasts.push(std::mem::take(&mut self.editing)); } - Command::none() + Task::none() } Message::Close(index) => { self.toasts.remove(index); - Command::none() + Task::none() } Message::Title(title) => { self.editing.title = title; - Command::none() + Task::none() } Message::Body(body) => { self.editing.body = body; - Command::none() + Task::none() } Message::Status(status) => { self.editing.status = status; - Command::none() + Task::none() } Message::Timeout(timeout) => { self.timeout_secs = timeout as u64; - Command::none() + Task::none() } Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(key::Named::Tab), @@ -88,7 +88,7 @@ impl App { key: keyboard::Key::Named(key::Named::Tab), .. })) => widget::focus_next(), - Message::Event(_) => Command::none(), + Message::Event(_) => Task::none(), } } @@ -347,7 +347,7 @@ mod toast { state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -589,7 +589,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.toasts diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index dd1e5213..c21e1a96 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ scrollable, text, text_input, Text, }; use iced::window; -use iced::{Command, Element, Font, Length, Subscription}; +use iced::{Element, Font, Length, Subscription, Task as Command}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 8030f5b4..b43c0cca 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Color, Command, Element, Font, Length, Point, Rectangle, - Subscription, Theme, + Alignment, Color, Element, Font, Length, Point, Rectangle, Subscription, + Task, Theme, }; pub fn main() -> iced::Result { @@ -33,14 +33,14 @@ enum Message { } impl Example { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::MouseMoved(position) => { self.mouse_position = Some(position); - Command::none() + Task::none() } - Message::Scrolled | Message::WindowResized => Command::batch(vec![ + Message::Scrolled | Message::WindowResized => Task::batch(vec![ container::visible_bounds(OUTER_CONTAINER.clone()) .map(Message::OuterBoundsFetched), container::visible_bounds(INNER_CONTAINER.clone()) @@ -49,12 +49,12 @@ impl Example { Message::OuterBoundsFetched(outer_bounds) => { self.outer_bounds = outer_bounds; - Command::none() + Task::none() } Message::InnerBoundsFetched(inner_bounds) => { self.inner_bounds = inner_bounds; - Command::none() + Task::none() } } } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index ba1e1029..8c0fa1d0 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -4,7 +4,7 @@ use iced::alignment::{self, Alignment}; use iced::widget::{ self, button, center, column, row, scrollable, text, text_input, }; -use iced::{color, Command, Element, Length, Subscription}; +use iced::{color, Element, Length, Subscription, Task}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { @@ -30,19 +30,19 @@ enum Message { } impl WebSocket { - fn load() -> Command { - Command::batch([ - Command::perform(echo::server::run(), |_| Message::Server), + fn load() -> Task { + Task::batch([ + Task::perform(echo::server::run(), |_| Message::Server), widget::focus_next(), ]) } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) -> Task { match message { Message::NewMessageChanged(new_message) => { self.new_message = new_message; - Command::none() + Task::none() } Message::Send(message) => match &mut self.state { State::Connected(connection) => { @@ -50,9 +50,9 @@ impl WebSocket { connection.send(message); - Command::none() + Task::none() } - State::Disconnected => Command::none(), + State::Disconnected => Task::none(), }, Message::Echo(event) => match event { echo::Event::Connected(connection) => { @@ -60,14 +60,14 @@ impl WebSocket { self.messages.push(echo::Message::connected()); - Command::none() + Task::none() } echo::Event::Disconnected => { self.state = State::Disconnected; self.messages.push(echo::Message::disconnected()); - Command::none() + Task::none() } echo::Event::MessageReceived(message) => { self.messages.push(message); @@ -78,7 +78,7 @@ impl WebSocket { ) } }, - Message::Server => Command::none(), + Message::Server => Task::none(), } } diff --git a/futures/src/event.rs b/futures/src/event.rs index 72ea78ad..ab895fcd 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -1,8 +1,8 @@ //! Listen to runtime events. use crate::core::event::{self, Event}; use crate::core::window; +use crate::core::MaybeSend; use crate::subscription::{self, Subscription}; -use crate::MaybeSend; /// Returns a [`Subscription`] to all the ignored runtime events. /// diff --git a/futures/src/executor.rs b/futures/src/executor.rs index 5ac76081..a9dde465 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -1,5 +1,5 @@ //! Choose your preferred executor to power a runtime. -use crate::MaybeSend; +use crate::core::MaybeSend; use futures::Future; /// A type that can run futures. diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index f0d7d757..c86e2169 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -2,8 +2,8 @@ use crate::core; use crate::core::event; use crate::core::keyboard::{Event, Key, Modifiers}; +use crate::core::MaybeSend; use crate::subscription::{self, Subscription}; -use crate::MaybeSend; /// Listens to keyboard key presses and calls the given function /// map them into actual messages. diff --git a/futures/src/lib.rs b/futures/src/lib.rs index a874a618..01b56306 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -8,7 +8,6 @@ pub use futures; pub use iced_core as core; -mod maybe; mod runtime; pub mod backend; @@ -18,7 +17,6 @@ pub mod keyboard; pub mod subscription; pub use executor::Executor; -pub use maybe::{MaybeSend, MaybeSync}; pub use platform::*; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 157e2c67..045fde6c 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,6 +1,7 @@ //! Run commands and keep track of subscriptions. +use crate::core::MaybeSend; use crate::subscription; -use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; +use crate::{BoxFuture, BoxStream, Executor}; use futures::{channel::mpsc, Sink}; use std::marker::PhantomData; diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 316fc44d..85a8a787 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -5,8 +5,9 @@ pub use tracker::Tracker; use crate::core::event; use crate::core::window; +use crate::core::MaybeSend; use crate::futures::{Future, Stream}; -use crate::{BoxStream, MaybeSend}; +use crate::BoxStream; use futures::channel::mpsc; use futures::never::Never; diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index f17e3ea3..c5a7bb99 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,5 +1,6 @@ +use crate::core::MaybeSend; use crate::subscription::{Event, Hasher, Recipe}; -use crate::{BoxFuture, MaybeSend}; +use crate::BoxFuture; use futures::channel::mpsc; use futures::sink::{Sink, SinkExt}; diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 47521eb0..4ac90f92 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -1,7 +1,6 @@ //! A compositor is responsible for initializing a renderer and managing window //! surfaces. -use crate::core::Color; -use crate::futures::{MaybeSend, MaybeSync}; +use crate::core::{Color, MaybeSend, MaybeSync}; use crate::{Error, Settings, Viewport}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index dd47c47d..19950d01 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,80 +1,62 @@ //! Access the clipboard. -use crate::command::{self, Command}; use crate::core::clipboard::Kind; -use crate::futures::MaybeSend; +use crate::futures::futures::channel::oneshot; +use crate::Task; -use std::fmt; - -/// A clipboard action to be performed by some [`Command`]. +/// A clipboard action to be performed by some [`Task`]. /// -/// [`Command`]: crate::Command -pub enum Action { +/// [`Task`]: crate::Task +#[derive(Debug)] +pub enum Action { /// Read the clipboard and produce `T` with the result. - Read(Box) -> T>, Kind), + Read { + /// The clipboard target. + target: Kind, + /// The channel to send the read contents. + channel: oneshot::Sender>, + }, /// Write the given contents to the clipboard. - Write(String, Kind), -} - -impl Action { - /// Maps the output of a clipboard [`Action`] using the provided closure. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - T: 'static, - { - match self { - Self::Read(o, target) => { - Action::Read(Box::new(move |s| f(o(s))), target) - } - Self::Write(content, target) => Action::Write(content, target), - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Read(_, target) => write!(f, "Action::Read{target:?}"), - Self::Write(_, target) => write!(f, "Action::Write({target:?})"), - } - } + Write { + /// The clipboard target. + target: Kind, + /// The contents to be written. + contents: String, + }, } /// Read the current contents of the clipboard. -pub fn read( - f: impl Fn(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Clipboard(Action::Read( - Box::new(f), - Kind::Standard, - ))) +pub fn read() -> Task> { + Task::oneshot(|channel| { + crate::Action::Clipboard(Action::Read { + target: Kind::Standard, + channel, + }) + }) } /// Read the current contents of the primary clipboard. -pub fn read_primary( - f: impl Fn(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Clipboard(Action::Read( - Box::new(f), - Kind::Primary, - ))) +pub fn read_primary() -> Task> { + Task::oneshot(|channel| { + crate::Action::Clipboard(Action::Read { + target: Kind::Primary, + channel, + }) + }) } /// Write the given contents to the clipboard. -pub fn write(contents: String) -> Command { - Command::single(command::Action::Clipboard(Action::Write( +pub fn write(contents: String) -> Task { + Task::effect(crate::Action::Clipboard(Action::Write { + target: Kind::Standard, contents, - Kind::Standard, - ))) + })) } /// Write the given contents to the primary clipboard. -pub fn write_primary(contents: String) -> Command { - Command::single(command::Action::Clipboard(Action::Write( +pub fn write_primary(contents: String) -> Task { + Task::effect(crate::Action::Clipboard(Action::Write { + target: Kind::Primary, contents, - Kind::Primary, - ))) + })) } diff --git a/runtime/src/command.rs b/runtime/src/command.rs deleted file mode 100644 index f7a746fe..00000000 --- a/runtime/src/command.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Run asynchronous actions. -mod action; - -pub use action::Action; - -use crate::core::widget; -use crate::futures::futures; -use crate::futures::MaybeSend; - -use futures::channel::mpsc; -use futures::Stream; -use std::fmt; -use std::future::Future; - -/// A set of asynchronous actions to be performed by some runtime. -#[must_use = "`Command` must be returned to runtime to take effect"] -pub struct Command(Internal>); - -#[derive(Debug)] -enum Internal { - None, - Single(T), - Batch(Vec), -} - -impl Command { - /// Creates an empty [`Command`]. - /// - /// In other words, a [`Command`] that does nothing. - pub const fn none() -> Self { - Self(Internal::None) - } - - /// Creates a [`Command`] that performs a single [`Action`]. - pub const fn single(action: Action) -> Self { - Self(Internal::Single(action)) - } - - /// Creates a [`Command`] that performs a [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation + 'static) -> Self { - Self::single(Action::Widget(Box::new(operation))) - } - - /// Creates a [`Command`] that performs the action of the given future. - pub fn perform( - future: impl Future + 'static + MaybeSend, - f: impl FnOnce(A) -> T + 'static + MaybeSend, - ) -> Command { - use futures::FutureExt; - - Command::single(Action::Future(Box::pin(future.map(f)))) - } - - /// Creates a [`Command`] that runs the given stream to completion. - pub fn run( - stream: impl Stream + 'static + MaybeSend, - f: impl Fn(A) -> T + 'static + MaybeSend, - ) -> Command { - use futures::StreamExt; - - Command::single(Action::Stream(Box::pin(stream.map(f)))) - } - - /// Creates a [`Command`] that performs the actions of all the given - /// commands. - /// - /// Once this command is run, all the commands will be executed at once. - pub fn batch(commands: impl IntoIterator>) -> Self { - let mut batch = Vec::new(); - - for Command(command) in commands { - match command { - Internal::None => {} - Internal::Single(command) => batch.push(command), - Internal::Batch(commands) => batch.extend(commands), - } - } - - Self(Internal::Batch(batch)) - } - - /// Applies a transformation to the result of a [`Command`]. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync + Clone, - ) -> Command - where - T: 'static, - A: 'static, - { - match self.0 { - Internal::None => Command::none(), - Internal::Single(action) => Command::single(action.map(f)), - Internal::Batch(batch) => Command(Internal::Batch( - batch - .into_iter() - .map(|action| action.map(f.clone())) - .collect(), - )), - } - } - - /// Returns all of the actions of the [`Command`]. - pub fn actions(self) -> Vec> { - let Command(command) = self; - - match command { - Internal::None => Vec::new(), - Internal::Single(action) => vec![action], - Internal::Batch(batch) => batch, - } - } -} - -impl From<()> for Command { - fn from(_value: ()) -> Self { - Self::none() - } -} - -impl fmt::Debug for Command { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Command(command) = self; - - command.fmt(f) - } -} - -/// Creates a [`Command`] that produces the `Message`s published from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -pub fn channel( - size: usize, - f: impl FnOnce(mpsc::Sender) -> Fut + MaybeSend + 'static, -) -> Command -where - Fut: Future + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::future; - use futures::stream::{self, StreamExt}; - - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).filter_map(|_| future::ready(None)); - - Command::single(Action::Stream(Box::pin(stream::select(receiver, runner)))) -} diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs deleted file mode 100644 index c9ffe801..00000000 --- a/runtime/src/command/action.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::clipboard; -use crate::core::widget; -use crate::font; -use crate::futures::MaybeSend; -use crate::system; -use crate::window; - -use std::any::Any; -use std::borrow::Cow; -use std::fmt; - -/// An action that a [`Command`] can perform. -/// -/// [`Command`]: crate::Command -pub enum Action { - /// Run a [`Future`] to completion. - /// - /// [`Future`]: iced_futures::BoxFuture - Future(iced_futures::BoxFuture), - - /// Run a [`Stream`] to completion. - /// - /// [`Stream`]: iced_futures::BoxStream - Stream(iced_futures::BoxStream), - - /// Run a clipboard action. - Clipboard(clipboard::Action), - - /// Run a window action. - Window(window::Action), - - /// Run a system action. - System(system::Action), - - /// Run a widget action. - Widget(Box>), - - /// Load a font from its bytes. - LoadFont { - /// The bytes of the font to load. - bytes: Cow<'static, [u8]>, - - /// The message to produce when the font has been loaded. - tagger: Box) -> T>, - }, - - /// A custom action supported by a specific runtime. - Custom(Box), -} - -impl Action { - /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: crate::Command - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - A: 'static, - T: 'static, - { - use iced_futures::futures::{FutureExt, StreamExt}; - - match self { - Self::Future(future) => Action::Future(Box::pin(future.map(f))), - Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))), - Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(window) => Action::Window(window.map(f)), - Self::System(system) => Action::System(system.map(f)), - Self::Widget(operation) => { - Action::Widget(Box::new(widget::operation::map(operation, f))) - } - Self::LoadFont { bytes, tagger } => Action::LoadFont { - bytes, - tagger: Box::new(move |result| f(tagger(result))), - }, - Self::Custom(custom) => Action::Custom(custom), - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Future(_) => write!(f, "Action::Future"), - Self::Stream(_) => write!(f, "Action::Stream"), - Self::Clipboard(action) => { - write!(f, "Action::Clipboard({action:?})") - } - Self::Window(action) => { - write!(f, "Action::Window({action:?})") - } - Self::System(action) => write!(f, "Action::System({action:?})"), - Self::Widget(_action) => write!(f, "Action::Widget"), - Self::LoadFont { .. } => write!(f, "Action::LoadFont"), - Self::Custom(_) => write!(f, "Action::Custom"), - } - } -} diff --git a/runtime/src/font.rs b/runtime/src/font.rs index 15359694..d54eb6a8 100644 --- a/runtime/src/font.rs +++ b/runtime/src/font.rs @@ -1,7 +1,5 @@ //! Load and use fonts. -pub use iced_core::font::*; - -use crate::command::{self, Command}; +use crate::{Action, Task}; use std::borrow::Cow; /// An error while loading a font. @@ -9,11 +7,9 @@ use std::borrow::Cow; pub enum Error {} /// Load a font from its bytes. -pub fn load( - bytes: impl Into>, -) -> Command> { - Command::single(command::Action::LoadFont { +pub fn load(bytes: impl Into>) -> Task> { + Task::oneshot(|channel| Action::LoadFont { bytes: bytes.into(), - tagger: Box::new(std::convert::identity), + channel, }) } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5f054c46..5fde3039 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,7 +10,6 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; -pub mod command; pub mod font; pub mod keyboard; pub mod overlay; @@ -22,6 +21,8 @@ pub mod window; #[cfg(feature = "multi-window")] pub mod multi_window; +mod task; + // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] @@ -34,8 +35,81 @@ mod debug; pub use iced_core as core; pub use iced_futures as futures; -pub use command::Command; pub use debug::Debug; -pub use font::Font; pub use program::Program; +pub use task::Task; pub use user_interface::UserInterface; + +use crate::core::widget; +use crate::futures::futures::channel::oneshot; + +use std::borrow::Cow; +use std::fmt; + +/// An action that the iced runtime can perform. +pub enum Action { + /// Output some value. + Output(T), + + /// Load a font from its bytes. + LoadFont { + /// The bytes of the font to load. + bytes: Cow<'static, [u8]>, + /// The channel to send back the load result. + channel: oneshot::Sender>, + }, + + /// Run a widget operation. + Widget(Box + Send>), + + /// Run a clipboard action. + Clipboard(clipboard::Action), + + /// Run a window action. + Window(window::Action), + + /// Run a system action. + System(system::Action), +} + +impl Action { + /// Creates a new [`Action::Widget`] with the given [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation<()> + 'static) -> Self { + Self::Widget(Box::new(operation)) + } + + fn output(self) -> Result> { + match self { + Action::Output(output) => Ok(output), + Action::LoadFont { bytes, channel } => { + Err(Action::LoadFont { bytes, channel }) + } + Action::Widget(operation) => Err(Action::Widget(operation)), + Action::Clipboard(action) => Err(Action::Clipboard(action)), + Action::Window(action) => Err(Action::Window(action)), + Action::System(action) => Err(Action::System(action)), + } + } +} + +impl fmt::Debug for Action +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Output(output) => write!(f, "Action::Output({output:?})"), + Action::LoadFont { .. } => { + write!(f, "Action::LoadFont") + } + Action::Widget { .. } => { + write!(f, "Action::Widget") + } + Action::Clipboard(action) => { + write!(f, "Action::Clipboard({action:?})") + } + Action::Window(_) => write!(f, "Action::Window"), + Action::System(action) => write!(f, "Action::System({action:?})"), + } + } +} diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs index 963a09d7..e8c71b26 100644 --- a/runtime/src/multi_window/program.rs +++ b/runtime/src/multi_window/program.rs @@ -2,7 +2,7 @@ use crate::core::text; use crate::core::window; use crate::core::{Element, Renderer}; -use crate::Command; +use crate::Task; /// The core of a user interface for a multi-window application following The Elm Architecture. pub trait Program: Sized { @@ -21,9 +21,9 @@ pub trait Program: Sized { /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the - /// background by shells. - fn update(&mut self, message: Self::Message) -> Command; + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task; /// Returns the widgets to display in the [`Program`] for the `window`. /// diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 10366ec0..72ce6933 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -5,7 +5,7 @@ use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{Debug, Program, Task}; /// The execution state of a multi-window [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -85,7 +85,7 @@ where /// the widgets of the linked [`Program`] if necessary. /// /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Command`] obtained from [`Program`] + /// captured by any widget, and the [`Task`] obtained from [`Program`] /// after updating it, only if an update was necessary. pub fn update( &mut self, @@ -96,7 +96,7 @@ where style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, - ) -> (Vec, Option>) { + ) -> (Vec, Option>) { let mut user_interfaces = build_user_interfaces( &self.program, self.caches.take().unwrap(), @@ -163,14 +163,14 @@ where drop(user_interfaces); - let commands = Command::batch(messages.into_iter().map(|msg| { + let commands = Task::batch(messages.into_iter().map(|msg| { debug.log_message(&msg); debug.update_started(); - let command = self.program.update(msg); + let task = self.program.update(msg); debug.update_finished(); - command + task })); let mut user_interfaces = build_user_interfaces( @@ -205,7 +205,7 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator>>, + operations: impl Iterator>>, bounds: Size, debug: &mut Debug, ) { @@ -227,9 +227,7 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { - self.queued_messages.push(message); - } + operation::Outcome::Some(()) => {} operation::Outcome::Chain(next) => { current_operation = Some(next); } diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index ddb9532b..11eee41c 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -131,13 +131,13 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { fn recurse( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) where Renderer: renderer::Renderer, { diff --git a/runtime/src/program.rs b/runtime/src/program.rs index 0ea94d3b..77acf497 100644 --- a/runtime/src/program.rs +++ b/runtime/src/program.rs @@ -1,5 +1,5 @@ //! Build interactive programs using The Elm Architecture. -use crate::Command; +use crate::Task; use iced_core::text; use iced_core::Element; @@ -25,9 +25,9 @@ pub trait Program: Sized { /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the + /// Any [`Task`] returned will be executed immediately in the /// background by shells. - fn update(&mut self, message: Self::Message) -> Command; + fn update(&mut self, message: Self::Message) -> Task; /// Returns the widgets to display in the [`Program`]. /// diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index c6589c22..e51ad0cb 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -4,7 +4,7 @@ use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{Debug, Program, Task}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -84,7 +84,7 @@ where /// the widgets of the linked [`Program`] if necessary. /// /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Command`] obtained from [`Program`] + /// captured by any widget, and the [`Task`] obtained from [`Program`] /// after updating it, only if an update was necessary. pub fn update( &mut self, @@ -95,7 +95,7 @@ where style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, - ) -> (Vec, Option>) { + ) -> (Vec, Option>) { let mut user_interface = build_user_interface( &mut self.program, self.cache.take().unwrap(), @@ -129,7 +129,7 @@ where messages.append(&mut self.queued_messages); debug.event_processing_finished(); - let command = if messages.is_empty() { + let task = if messages.is_empty() { debug.draw_started(); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); @@ -143,16 +143,15 @@ where // for now :^) let temp_cache = user_interface.into_cache(); - let commands = - Command::batch(messages.into_iter().map(|message| { - debug.log_message(&message); + let tasks = Task::batch(messages.into_iter().map(|message| { + debug.log_message(&message); - debug.update_started(); - let command = self.program.update(message); - debug.update_finished(); + debug.update_started(); + let task = self.program.update(message); + debug.update_finished(); - command - })); + task + })); let mut user_interface = build_user_interface( &mut self.program, @@ -169,17 +168,17 @@ where self.cache = Some(user_interface.into_cache()); - Some(commands) + Some(tasks) }; - (uncaptured_events, command) + (uncaptured_events, task) } /// Applies [`Operation`]s to the [`State`] pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator>>, + operations: impl Iterator>>, bounds: Size, debug: &mut Debug, ) { @@ -199,9 +198,7 @@ where match operation.finish() { operation::Outcome::None => {} - operation::Outcome::Some(message) => { - self.queued_messages.push(message); - } + operation::Outcome::Some(()) => {} operation::Outcome::Chain(next) => { current_operation = Some(next); } diff --git a/runtime/src/system.rs b/runtime/src/system.rs index 61c8ff29..b6fb4fdf 100644 --- a/runtime/src/system.rs +++ b/runtime/src/system.rs @@ -1,6 +1,39 @@ //! Access the native system. -mod action; -mod information; +use crate::futures::futures::channel::oneshot; -pub use action::Action; -pub use information::Information; +/// An operation to be performed on the system. +#[derive(Debug)] +pub enum Action { + /// Query system information and produce `T` with the result. + QueryInformation(oneshot::Sender), +} + +/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). +#[derive(Clone, Debug)] +pub struct Information { + /// The operating system name + pub system_name: Option, + /// Operating system kernel version + pub system_kernel: Option, + /// Long operating system version + /// + /// Examples: + /// - MacOS 10.15 Catalina + /// - Windows 10 Pro + /// - Ubuntu 20.04 LTS (Focal Fossa) + pub system_version: Option, + /// Short operating system version number + pub system_short_version: Option, + /// Detailed processor model information + pub cpu_brand: String, + /// The number of physical cores on the processor + pub cpu_cores: Option, + /// Total RAM size, in bytes + pub memory_total: u64, + /// Memory used by this process, in bytes + pub memory_used: Option, + /// Underlying graphics backend for rendering + pub graphics_backend: String, + /// Model information for the active graphics adapter + pub graphics_adapter: String, +} diff --git a/runtime/src/system/action.rs b/runtime/src/system/action.rs deleted file mode 100644 index dea9536f..00000000 --- a/runtime/src/system/action.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::system; - -use iced_futures::MaybeSend; -use std::fmt; - -/// An operation to be performed on the system. -pub enum Action { - /// Query system information and produce `T` with the result. - QueryInformation(Box>), -} - -pub trait Closure: Fn(system::Information) -> T + MaybeSend {} - -impl Closure for T where T: Fn(system::Information) -> O + MaybeSend {} - -impl Action { - /// Maps the output of a system [`Action`] using the provided closure. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - T: 'static, - { - match self { - Self::QueryInformation(o) => { - Action::QueryInformation(Box::new(move |s| f(o(s)))) - } - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::QueryInformation(_) => write!(f, "Action::QueryInformation"), - } - } -} diff --git a/runtime/src/system/information.rs b/runtime/src/system/information.rs deleted file mode 100644 index 0f78f5e9..00000000 --- a/runtime/src/system/information.rs +++ /dev/null @@ -1,29 +0,0 @@ -/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). -#[derive(Clone, Debug)] -pub struct Information { - /// The operating system name - pub system_name: Option, - /// Operating system kernel version - pub system_kernel: Option, - /// Long operating system version - /// - /// Examples: - /// - MacOS 10.15 Catalina - /// - Windows 10 Pro - /// - Ubuntu 20.04 LTS (Focal Fossa) - pub system_version: Option, - /// Short operating system version number - pub system_short_version: Option, - /// Detailed processor model information - pub cpu_brand: String, - /// The number of physical cores on the processor - pub cpu_cores: Option, - /// Total RAM size, in bytes - pub memory_total: u64, - /// Memory used by this process, in bytes - pub memory_used: Option, - /// Underlying graphics backend for rendering - pub graphics_backend: String, - /// Model information for the active graphics adapter - pub graphics_adapter: String, -} diff --git a/runtime/src/task.rs b/runtime/src/task.rs new file mode 100644 index 00000000..ac28a4e7 --- /dev/null +++ b/runtime/src/task.rs @@ -0,0 +1,214 @@ +use crate::core::widget; +use crate::core::MaybeSend; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::future::{self, FutureExt}; +use crate::futures::futures::never::Never; +use crate::futures::futures::stream::{self, Stream, StreamExt}; +use crate::futures::{boxed_stream, BoxStream}; +use crate::Action; + +use std::future::Future; + +/// 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)] +pub struct Task(Option>>); + +impl Task { + /// Creates a [`Task`] that does nothing. + pub fn none() -> Self { + Self(None) + } + + /// Creates a new [`Task`] that instantly produces the given value. + pub fn done(value: T) -> Self + where + T: MaybeSend + 'static, + { + Self::future(future::ready(value)) + } + + /// Creates a new [`Task`] that runs the given [`Future`] and produces + /// its output. + pub fn future(future: impl Future + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self::stream(stream::once(future)) + } + + /// Creates a new [`Task`] that runs the given [`Stream`] and produces + /// each of its items. + pub fn stream(stream: impl Stream + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream.map(Action::Output)))) + } + + /// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces + /// its output. + pub fn widget(operation: impl widget::Operation + 'static) -> Task + where + T: MaybeSend + 'static, + { + Self::channel(move |sender| { + let operation = + widget::operation::map(Box::new(operation), move |value| { + let _ = sender.clone().try_send(value); + }); + + Action::Widget(Box::new(operation)) + }) + } + + /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and + /// produces the value fed to the [`oneshot::Sender`]. + pub fn oneshot(f: impl FnOnce(oneshot::Sender) -> Action) -> Task + where + T: MaybeSend + 'static, + { + let (sender, receiver) = oneshot::channel(); + + let action = f(sender); + + Self(Some(boxed_stream( + stream::once(async move { action }).chain( + receiver.into_stream().filter_map(|result| async move { + Some(Action::Output(result.ok()?)) + }), + ), + ))) + } + + /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and + /// produces the values fed to the [`mpsc::Sender`]. + pub fn channel(f: impl FnOnce(mpsc::Sender) -> Action) -> Task + where + T: MaybeSend + 'static, + { + let (sender, receiver) = mpsc::channel(1); + + let action = f(sender); + + Self(Some(boxed_stream( + stream::once(async move { action }) + .chain(receiver.map(|result| Action::Output(result))), + ))) + } + + /// Creates a new [`Task`] that executes the given [`Action`] and produces no output. + pub fn effect(action: impl Into>) -> Self { + let action = action.into(); + + Self(Some(boxed_stream(stream::once(async move { + action.output().expect_err("no output") + })))) + } + + /// Maps the output of a [`Task`] with the given closure. + pub fn map( + self, + mut f: impl FnMut(T) -> O + MaybeSend + 'static, + ) -> Task + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + self.then(move |output| Task::done(f(output))) + } + + /// Performs a new [`Task`] for every output of the current [`Task`] using the + /// given closure. + /// + /// This is the monadic interface of [`Task`]—analogous to [`Future`] and + /// [`Stream`]. + pub fn then( + self, + mut f: impl FnMut(T) -> Task + MaybeSend + 'static, + ) -> Task + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + Task(match self.0 { + None => None, + Some(stream) => { + Some(boxed_stream(stream.flat_map(move |action| { + match action.output() { + Ok(output) => f(output) + .0 + .unwrap_or_else(|| boxed_stream(stream::empty())), + Err(action) => { + boxed_stream(stream::once(async move { action })) + } + } + }))) + } + }) + } + + /// Chains a new [`Task`] to be performed once the current one finishes completely. + pub fn chain(self, task: Self) -> Self + where + T: 'static, + { + match self.0 { + None => task, + Some(first) => match task.0 { + None => Task::none(), + Some(second) => Task(Some(boxed_stream(first.chain(second)))), + }, + } + } + + /// Creates a [`Task`] that runs the given [`Future`] to completion. + pub fn perform( + future: impl Future + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: MaybeSend + 'static, + A: MaybeSend + 'static, + { + Self::future(future.map(f)) + } + + /// Creates a [`Task`] that runs the given [`Stream`] to completion. + pub fn run( + stream: impl Stream + MaybeSend + 'static, + f: impl Fn(A) -> T + 'static + MaybeSend, + ) -> Self + where + T: 'static, + { + Self::stream(stream.map(f)) + } + + /// Combines the given tasks and produces a single [`Task`] that will run all of them + /// in parallel. + pub fn batch(tasks: impl IntoIterator) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream::select_all( + tasks.into_iter().filter_map(|task| task.0), + )))) + } + + /// Returns the underlying [`Stream`] of the [`Task`]. + pub fn into_stream(self) -> Option>> { + self.0 + } +} + +impl From<()> for Task +where + T: MaybeSend + 'static, +{ + fn from(_value: ()) -> Self { + Self::none() + } +} diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 006225ed..858b1a2d 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -566,7 +566,7 @@ where pub fn operate( &mut self, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { self.root.as_widget().operate( &mut self.state, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index b68c9a71..0876ab69 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,24 +1,145 @@ //! Build window-based GUI applications. -mod action; - pub mod screenshot; -pub use action::Action; pub use screenshot::Screenshot; -use crate::command::{self, Command}; use crate::core::time::Instant; use crate::core::window::{ Event, Icon, Id, Level, Mode, Settings, UserAttention, }; -use crate::core::{Point, Size}; +use crate::core::{MaybeSend, Point, Size}; use crate::futures::event; +use crate::futures::futures::channel::oneshot; use crate::futures::Subscription; +use crate::Task; pub use raw_window_handle; use raw_window_handle::WindowHandle; +/// An operation to be performed on some window. +#[allow(missing_debug_implementations)] +pub enum Action { + /// Opens a new window with some [`Settings`]. + Open(Id, Settings), + + /// Close the window and exits the application. + Close(Id), + + /// Move the window with the left mouse button until the button is + /// released. + /// + /// There’s no guarantee that this will work unless the left mouse + /// button was pressed immediately before this function is called. + Drag(Id), + + /// Resize the window to the given logical dimensions. + Resize(Id, Size), + + /// Fetch the current logical dimensions of the window. + FetchSize(Id, oneshot::Sender), + + /// Fetch if the current window is maximized or not. + FetchMaximized(Id, oneshot::Sender), + + /// Set the window to maximized or back + Maximize(Id, bool), + + /// Fetch if the current window is minimized or not. + /// + /// ## Platform-specific + /// - **Wayland:** Always `None`. + FetchMinimized(Id, oneshot::Sender>), + + /// Set the window to minimized or back + Minimize(Id, bool), + + /// Fetch the current logical coordinates of the window. + FetchPosition(Id, oneshot::Sender>), + + /// Move the window to the given logical coordinates. + /// + /// Unsupported on Wayland. + Move(Id, Point), + + /// Change the [`Mode`] of the window. + ChangeMode(Id, Mode), + + /// Fetch the current [`Mode`] of the window. + FetchMode(Id, oneshot::Sender), + + /// Toggle the window to maximized or back + ToggleMaximize(Id), + + /// Toggle whether window has decorations. + /// + /// ## Platform-specific + /// - **X11:** Not implemented. + /// - **Web:** Unsupported. + ToggleDecorations(Id), + + /// Request user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see [`UserAttention`] for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. + /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. + RequestUserAttention(Id, Option), + + /// Bring the window to the front and sets input focus. Has no effect if the window is + /// already in focus, minimized, or not visible. + /// + /// This method steals input focus from other applications. Do not use this method unless + /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive + /// user experience. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland:** Unsupported. + GainFocus(Id), + + /// Change the window [`Level`]. + ChangeLevel(Id, Level), + + /// Show the system menu at cursor position. + /// + /// ## Platform-specific + /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. + ShowSystemMenu(Id), + + /// Fetch the raw identifier unique to the window. + FetchRawId(Id, oneshot::Sender), + + /// Change the window [`Icon`]. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland / macOS:** Unsupported. + /// + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That + /// said, it's usually in the same ballpark as on Windows. + ChangeIcon(Id, Icon), + + /// Runs the closure with the native window handle of the window with the given [`Id`]. + RunWithHandle(Id, Box) + Send>), + + /// Screenshot the viewport of the window. + Screenshot(Id, oneshot::Sender), +} + /// Subscribes to the frames of the window of the running application. /// /// The resulting [`Subscription`] will produce items at a rate equal to the @@ -34,110 +155,96 @@ pub fn frames() -> Subscription { }) } -/// Spawns a new window with the given `settings`. +/// Opens a new window with the given `settings`. /// -/// Returns the new window [`Id`] alongside the [`Command`]. -pub fn spawn(settings: Settings) -> (Id, Command) { +/// Returns the new window [`Id`] alongside the [`Task`]. +pub fn open(settings: Settings) -> (Id, Task) { let id = Id::unique(); ( id, - Command::single(command::Action::Window(Action::Spawn(id, settings))), + Task::effect(crate::Action::Window(Action::Open(id, settings))), ) } /// Closes the window with `id`. -pub fn close(id: Id) -> Command { - Command::single(command::Action::Window(Action::Close(id))) +pub fn close(id: Id) -> Task { + Task::effect(crate::Action::Window(Action::Close(id))) } /// Begins dragging the window while the left mouse button is held. -pub fn drag(id: Id) -> Command { - Command::single(command::Action::Window(Action::Drag(id))) +pub fn drag(id: Id) -> Task { + Task::effect(crate::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. -pub fn resize(id: Id, new_size: Size) -> Command { - Command::single(command::Action::Window(Action::Resize(id, new_size))) +pub fn resize(id: Id, new_size: Size) -> Task { + Task::effect(crate::Action::Window(Action::Resize(id, new_size))) } /// Fetches the window's size in logical dimensions. -pub fn fetch_size( - id: Id, - f: impl FnOnce(Size) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) +pub fn fetch_size(id: Id) -> Task { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchSize(id, channel)) + }) } /// Fetches if the window is maximized. -pub fn fetch_maximized( - id: Id, - f: impl FnOnce(bool) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchMaximized( - id, - Box::new(f), - ))) +pub fn fetch_maximized(id: Id) -> Task { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchMaximized(id, channel)) + }) } /// Maximizes the window. -pub fn maximize(id: Id, maximized: bool) -> Command { - Command::single(command::Action::Window(Action::Maximize(id, maximized))) +pub fn maximize(id: Id, maximized: bool) -> Task { + Task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } /// Fetches if the window is minimized. -pub fn fetch_minimized( - id: Id, - f: impl FnOnce(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchMinimized( - id, - Box::new(f), - ))) +pub fn fetch_minimized(id: Id) -> Task> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchMinimized(id, channel)) + }) } /// Minimizes the window. -pub fn minimize(id: Id, minimized: bool) -> Command { - Command::single(command::Action::Window(Action::Minimize(id, minimized))) +pub fn minimize(id: Id, minimized: bool) -> Task { + Task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } /// Fetches the current window position in logical coordinates. -pub fn fetch_position( - id: Id, - f: impl FnOnce(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchPosition( - id, - Box::new(f), - ))) +pub fn fetch_position(id: Id) -> Task> { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchPosition(id, channel)) + }) } /// Moves the window to the given logical coordinates. -pub fn move_to(id: Id, position: Point) -> Command { - Command::single(command::Action::Window(Action::Move(id, position))) +pub fn move_to(id: Id, position: Point) -> Task { + Task::effect(crate::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. -pub fn change_mode(id: Id, mode: Mode) -> Command { - Command::single(command::Action::Window(Action::ChangeMode(id, mode))) +pub fn change_mode(id: Id, mode: Mode) -> Task { + Task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) } /// Fetches the current [`Mode`] of the window. -pub fn fetch_mode( - id: Id, - f: impl FnOnce(Mode) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f)))) +pub fn fetch_mode(id: Id) -> Task { + Task::oneshot(move |channel| { + crate::Action::Window(Action::FetchMode(id, channel)) + }) } /// Toggles the window to maximized or back. -pub fn toggle_maximize(id: Id) -> Command { - Command::single(command::Action::Window(Action::ToggleMaximize(id))) +pub fn toggle_maximize(id: Id) -> Task { + Task::effect(crate::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. -pub fn toggle_decorations(id: Id) -> Command { - Command::single(command::Action::Window(Action::ToggleDecorations(id))) +pub fn toggle_decorations(id: Id) -> Task { + Task::effect(crate::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -146,11 +253,11 @@ pub fn toggle_decorations(id: Id) -> Command { /// /// Providing `None` will unset the request for user attention. Unsetting the request for /// user attention might not be done automatically by the WM when the window receives input. -pub fn request_user_attention( +pub fn request_user_attention( id: Id, user_attention: Option, -) -> Command { - Command::single(command::Action::Window(Action::RequestUserAttention( +) -> Task { + Task::effect(crate::Action::Window(Action::RequestUserAttention( id, user_attention, ))) @@ -159,59 +266,61 @@ pub fn request_user_attention( /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// -/// This [`Command`] steals input focus from other applications. Do not use this method unless +/// This [`Task`] steals input focus from other applications. Do not use this method unless /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. -pub fn gain_focus(id: Id) -> Command { - Command::single(command::Action::Window(Action::GainFocus(id))) +pub fn gain_focus(id: Id) -> Task { + Task::effect(crate::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. -pub fn change_level(id: Id, level: Level) -> Command { - Command::single(command::Action::Window(Action::ChangeLevel(id, level))) +pub fn change_level(id: Id, level: Level) -> Task { + Task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) } /// Show the [system menu] at cursor position. /// /// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu -pub fn show_system_menu(id: Id) -> Command { - Command::single(command::Action::Window(Action::ShowSystemMenu(id))) +pub fn show_system_menu(id: Id) -> Task { + Task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. -pub fn fetch_id( - id: Id, - f: impl FnOnce(u64) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchId(id, Box::new(f)))) +pub fn fetch_raw_id(id: Id) -> Task { + Task::oneshot(|channel| { + crate::Action::Window(Action::FetchRawId(id, channel)) + }) } /// Changes the [`Icon`] of the window. -pub fn change_icon(id: Id, icon: Icon) -> Command { - Command::single(command::Action::Window(Action::ChangeIcon(id, icon))) +pub fn change_icon(id: Id, icon: Icon) -> Task { + Task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) } /// Runs the given callback with the native window handle for the window with the given id. /// /// Note that if the window closes before this call is processed the callback will not be run. -pub fn run_with_handle( +pub fn run_with_handle( id: Id, - f: impl FnOnce(WindowHandle<'_>) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::RunWithHandle( - id, - Box::new(f), - ))) + f: impl FnOnce(WindowHandle<'_>) -> T + MaybeSend + 'static, +) -> Task +where + T: MaybeSend + 'static, +{ + Task::oneshot(move |channel| { + crate::Action::Window(Action::RunWithHandle( + id, + Box::new(move |handle| { + let _ = channel.send(f(handle)); + }), + )) + }) } /// Captures a [`Screenshot`] from the window. -pub fn screenshot( - id: Id, - f: impl FnOnce(Screenshot) -> Message + Send + 'static, -) -> Command { - Command::single(command::Action::Window(Action::Screenshot( - id, - Box::new(f), - ))) +pub fn screenshot(id: Id) -> Task { + Task::oneshot(move |channel| { + crate::Action::Window(Action::Screenshot(id, channel)) + }) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs deleted file mode 100644 index 07e77872..00000000 --- a/runtime/src/window/action.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention}; -use crate::core::{Point, Size}; -use crate::futures::MaybeSend; -use crate::window::Screenshot; - -use raw_window_handle::WindowHandle; - -use std::fmt; - -/// An operation to be performed on some window. -pub enum Action { - /// Spawns a new window with some [`Settings`]. - Spawn(Id, Settings), - /// Close the window and exits the application. - Close(Id), - /// Move the window with the left mouse button until the button is - /// released. - /// - /// There’s no guarantee that this will work unless the left mouse - /// button was pressed immediately before this function is called. - Drag(Id), - /// Resize the window to the given logical dimensions. - Resize(Id, Size), - /// Fetch the current logical dimensions of the window. - FetchSize(Id, Box T + 'static>), - /// Fetch if the current window is maximized or not. - /// - /// ## Platform-specific - /// - **iOS / Android / Web:** Unsupported. - FetchMaximized(Id, Box T + 'static>), - /// Set the window to maximized or back - Maximize(Id, bool), - /// Fetch if the current window is minimized or not. - /// - /// ## Platform-specific - /// - **Wayland:** Always `None`. - /// - **iOS / Android / Web:** Unsupported. - FetchMinimized(Id, Box) -> T + 'static>), - /// Set the window to minimized or back - Minimize(Id, bool), - /// Fetch the current logical coordinates of the window. - FetchPosition(Id, Box) -> T + 'static>), - /// Move the window to the given logical coordinates. - /// - /// Unsupported on Wayland. - Move(Id, Point), - /// Change the [`Mode`] of the window. - ChangeMode(Id, Mode), - /// Fetch the current [`Mode`] of the window. - FetchMode(Id, Box T + 'static>), - /// Toggle the window to maximized or back - ToggleMaximize(Id), - /// Toggle whether window has decorations. - /// - /// ## Platform-specific - /// - **X11:** Not implemented. - /// - **Web:** Unsupported. - ToggleDecorations(Id), - /// Request user attention to the window, this has no effect if the application - /// is already focused. How requesting for user attention manifests is platform dependent, - /// see [`UserAttention`] for details. - /// - /// Providing `None` will unset the request for user attention. Unsetting the request for - /// user attention might not be done automatically by the WM when the window receives input. - /// - /// ## Platform-specific - /// - /// - **iOS / Android / Web:** Unsupported. - /// - **macOS:** `None` has no effect. - /// - **X11:** Requests for user attention must be manually cleared. - /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. - RequestUserAttention(Id, Option), - /// Bring the window to the front and sets input focus. Has no effect if the window is - /// already in focus, minimized, or not visible. - /// - /// This method steals input focus from other applications. Do not use this method unless - /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive - /// user experience. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland:** Unsupported. - GainFocus(Id), - /// Change the window [`Level`]. - ChangeLevel(Id, Level), - /// Show the system menu at cursor position. - /// - /// ## Platform-specific - /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. - ShowSystemMenu(Id), - /// Fetch the raw identifier unique to the window. - FetchId(Id, Box T + 'static>), - /// Change the window [`Icon`]. - /// - /// On Windows and X11, this is typically the small icon in the top-left - /// corner of the titlebar. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland / macOS:** Unsupported. - /// - /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's - /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. - /// - /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That - /// said, it's usually in the same ballpark as on Windows. - ChangeIcon(Id, Icon), - /// Runs the closure with the native window handle of the window with the given [`Id`]. - RunWithHandle(Id, Box) -> T + 'static>), - /// Screenshot the viewport of the window. - Screenshot(Id, Box T + 'static>), -} - -impl Action { - /// Maps the output of a window [`Action`] using the provided closure. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - T: 'static, - { - match self { - Self::Spawn(id, settings) => Action::Spawn(id, settings), - Self::Close(id) => Action::Close(id), - Self::Drag(id) => Action::Drag(id), - Self::Resize(id, size) => Action::Resize(id, size), - Self::FetchSize(id, o) => { - Action::FetchSize(id, Box::new(move |s| f(o(s)))) - } - Self::FetchMaximized(id, o) => { - Action::FetchMaximized(id, Box::new(move |s| f(o(s)))) - } - Self::Maximize(id, maximized) => Action::Maximize(id, maximized), - Self::FetchMinimized(id, o) => { - Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) - } - Self::Minimize(id, minimized) => Action::Minimize(id, minimized), - Self::FetchPosition(id, o) => { - Action::FetchPosition(id, Box::new(move |s| f(o(s)))) - } - Self::Move(id, position) => Action::Move(id, position), - Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), - Self::FetchMode(id, o) => { - Action::FetchMode(id, Box::new(move |s| f(o(s)))) - } - Self::ToggleMaximize(id) => Action::ToggleMaximize(id), - Self::ToggleDecorations(id) => Action::ToggleDecorations(id), - Self::RequestUserAttention(id, attention_type) => { - Action::RequestUserAttention(id, attention_type) - } - Self::GainFocus(id) => Action::GainFocus(id), - Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), - Self::ShowSystemMenu(id) => Action::ShowSystemMenu(id), - Self::FetchId(id, o) => { - Action::FetchId(id, Box::new(move |s| f(o(s)))) - } - Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon), - Self::RunWithHandle(id, o) => { - Action::RunWithHandle(id, Box::new(move |s| f(o(s)))) - } - Self::Screenshot(id, tag) => Action::Screenshot( - id, - Box::new(move |screenshot| f(tag(screenshot))), - ), - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Spawn(id, settings) => { - write!(f, "Action::Spawn({id:?}, {settings:?})") - } - Self::Close(id) => write!(f, "Action::Close({id:?})"), - Self::Drag(id) => write!(f, "Action::Drag({id:?})"), - Self::Resize(id, size) => { - write!(f, "Action::Resize({id:?}, {size:?})") - } - Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), - Self::FetchMaximized(id, _) => { - write!(f, "Action::FetchMaximized({id:?})") - } - Self::Maximize(id, maximized) => { - write!(f, "Action::Maximize({id:?}, {maximized})") - } - Self::FetchMinimized(id, _) => { - write!(f, "Action::FetchMinimized({id:?})") - } - Self::Minimize(id, minimized) => { - write!(f, "Action::Minimize({id:?}, {minimized}") - } - Self::FetchPosition(id, _) => { - write!(f, "Action::FetchPosition({id:?})") - } - Self::Move(id, position) => { - write!(f, "Action::Move({id:?}, {position})") - } - Self::ChangeMode(id, mode) => { - write!(f, "Action::SetMode({id:?}, {mode:?})") - } - Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"), - Self::ToggleMaximize(id) => { - write!(f, "Action::ToggleMaximize({id:?})") - } - Self::ToggleDecorations(id) => { - write!(f, "Action::ToggleDecorations({id:?})") - } - Self::RequestUserAttention(id, _) => { - write!(f, "Action::RequestUserAttention({id:?})") - } - Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"), - Self::ChangeLevel(id, level) => { - write!(f, "Action::ChangeLevel({id:?}, {level:?})") - } - Self::ShowSystemMenu(id) => { - write!(f, "Action::ShowSystemMenu({id:?})") - } - Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), - Self::ChangeIcon(id, _icon) => { - write!(f, "Action::ChangeIcon({id:?})") - } - Self::RunWithHandle(id, _) => { - write!(f, "Action::RunWithHandle({id:?})") - } - Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"), - } - } -} diff --git a/src/application.rs b/src/application.rs index d12ba73d..4cd4a87d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -2,7 +2,7 @@ use crate::core::text; use crate::graphics::compositor; use crate::shell::application; -use crate::{Command, Element, Executor, Settings, Subscription}; +use crate::{Element, Executor, Settings, Subscription, Task}; pub use application::{Appearance, DefaultStyle}; @@ -16,7 +16,7 @@ pub use application::{Appearance, DefaultStyle}; /// document. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -62,7 +62,7 @@ pub use application::{Appearance, DefaultStyle}; /// ```no_run /// use iced::advanced::Application; /// use iced::executor; -/// use iced::{Command, Element, Settings, Theme, Renderer}; +/// use iced::{Task, Element, Settings, Theme, Renderer}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -77,16 +77,16 @@ pub use application::{Appearance, DefaultStyle}; /// type Theme = Theme; /// type Renderer = Renderer; /// -/// fn new(_flags: ()) -> (Hello, Command) { -/// (Hello, Command::none()) +/// fn new(_flags: ()) -> (Hello, Task) { +/// (Hello, Task::none()) /// } /// /// fn title(&self) -> String { /// String::from("A cool application") /// } /// -/// fn update(&mut self, _message: Self::Message) -> Command { -/// Command::none() +/// fn update(&mut self, _message: Self::Message) -> Task { +/// Task::none() /// } /// /// fn view(&self) -> Element { @@ -123,12 +123,12 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. /// /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command); + fn new(flags: Self::Flags) -> (Self, Task); /// Returns the current title of the [`Application`]. /// @@ -142,8 +142,8 @@ where /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command; + /// Any [`Task`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Task; /// Returns the widgets to display in the [`Application`]. /// @@ -234,7 +234,7 @@ where type Theme = A::Theme; type Renderer = A::Renderer; - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { self.0.update(message) } @@ -250,7 +250,7 @@ where { type Flags = A::Flags; - fn new(flags: Self::Flags) -> (Self, Command) { + fn new(flags: Self::Flags) -> (Self, Task) { let (app, command) = A::new(flags); (Instance(app), command) diff --git a/src/lib.rs b/src/lib.rs index 317d25a6..cf0bc7d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,6 +203,7 @@ pub use crate::core::{ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; +pub use crate::runtime::Task; pub mod clipboard { //! Access the clipboard. @@ -256,11 +257,6 @@ pub mod mouse { }; } -pub mod command { - //! Run asynchronous actions. - pub use crate::runtime::command::{channel, Command}; -} - pub mod subscription { //! Listen to external events in your application. pub use iced_futures::subscription::{ @@ -312,7 +308,6 @@ pub mod widget { mod runtime {} } -pub use command::Command; pub use error::Error; pub use event::Event; pub use executor::Executor; diff --git a/src/multi_window.rs b/src/multi_window.rs index b81297dc..4900bb85 100644 --- a/src/multi_window.rs +++ b/src/multi_window.rs @@ -1,6 +1,6 @@ //! Leverage multi-window support in your application. use crate::window; -use crate::{Command, Element, Executor, Settings, Subscription}; +use crate::{Element, Executor, Settings, Subscription, Task}; pub use crate::application::{Appearance, DefaultStyle}; @@ -14,7 +14,7 @@ pub use crate::application::{Appearance, DefaultStyle}; /// document and display only the contents of the `window::Id::MAIN` window. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -29,7 +29,7 @@ pub use crate::application::{Appearance, DefaultStyle}; /// /// ```no_run /// use iced::{executor, window}; -/// use iced::{Command, Element, Settings, Theme}; +/// use iced::{Task, Element, Settings, Theme}; /// use iced::multi_window::{self, Application}; /// /// pub fn main() -> iced::Result { @@ -44,16 +44,16 @@ pub use crate::application::{Appearance, DefaultStyle}; /// type Message = (); /// type Theme = Theme; /// -/// fn new(_flags: ()) -> (Hello, Command) { -/// (Hello, Command::none()) +/// fn new(_flags: ()) -> (Hello, Task) { +/// (Hello, Task::none()) /// } /// /// fn title(&self, _window: window::Id) -> String { /// String::from("A cool application") /// } /// -/// fn update(&mut self, _message: Self::Message) -> Command { -/// Command::none() +/// fn update(&mut self, _message: Self::Message) -> Task { +/// Task::none() /// } /// /// fn view(&self, _window: window::Id) -> Element { @@ -89,12 +89,12 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. /// /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command); + fn new(flags: Self::Flags) -> (Self, Task); /// Returns the current title of the `window` of the [`Application`]. /// @@ -108,8 +108,8 @@ where /// produced by either user interactions or commands, will be handled by /// this method. /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command; + /// Any [`Task`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Task; /// Returns the widgets to display in the `window` of the [`Application`]. /// @@ -207,7 +207,7 @@ where type Theme = A::Theme; type Renderer = crate::Renderer; - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { self.0.update(message) } @@ -226,7 +226,7 @@ where { type Flags = A::Flags; - fn new(flags: Self::Flags) -> (Self, Command) { + fn new(flags: Self::Flags) -> (Self, Task) { let (app, command) = A::new(flags); (Instance(app), command) diff --git a/src/program.rs b/src/program.rs index d4c2a266..ea6b0e8e 100644 --- a/src/program.rs +++ b/src/program.rs @@ -35,7 +35,7 @@ use crate::core::text; use crate::executor::{self, Executor}; use crate::graphics::compositor; use crate::window; -use crate::{Command, Element, Font, Result, Settings, Size, Subscription}; +use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; pub use crate::application::{Appearance, DefaultStyle}; @@ -76,7 +76,7 @@ pub fn program( ) -> Program> where State: 'static, - Message: Send + std::fmt::Debug, + Message: Send + std::fmt::Debug + 'static, Theme: Default + DefaultStyle, Renderer: self::Renderer, { @@ -94,7 +94,7 @@ where impl Definition for Application where - Message: Send + std::fmt::Debug, + Message: Send + std::fmt::Debug + 'static, Theme: Default + DefaultStyle, Renderer: self::Renderer, Update: self::Update, @@ -106,15 +106,15 @@ where type Renderer = Renderer; type Executor = executor::Default; - fn load(&self) -> Command { - Command::none() + fn load(&self) -> Task { + Task::none() } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command { + ) -> Task { self.update.update(state, message).into() } @@ -197,7 +197,7 @@ impl Program

{ fn new( (program, initialize): Self::Flags, - ) -> (Self, Command) { + ) -> (Self, Task) { let state = initialize(); let command = program.load(); @@ -218,7 +218,7 @@ impl Program

{ fn update( &mut self, message: Self::Message, - ) -> Command { + ) -> Task { self.program.update(&mut self.state, message) } @@ -357,10 +357,10 @@ impl Program

{ } } - /// Runs the [`Command`] produced by the closure at startup. + /// Runs the [`Task`] produced by the closure at startup. pub fn load( self, - f: impl Fn() -> Command, + f: impl Fn() -> Task, ) -> Program< impl Definition, > { @@ -420,7 +420,7 @@ pub trait Definition: Sized { type State; /// The message of the program. - type Message: Send + std::fmt::Debug; + type Message: Send + std::fmt::Debug + 'static; /// The theme of the program. type Theme: Default + DefaultStyle; @@ -431,13 +431,13 @@ pub trait Definition: Sized { /// The executor of the program. type Executor: Executor; - fn load(&self) -> Command; + fn load(&self) -> Task; fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command; + ) -> Task; fn view<'a>( &self, @@ -484,7 +484,7 @@ fn with_title( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Command { + fn load(&self) -> Task { self.program.load() } @@ -496,7 +496,7 @@ fn with_title( &self, state: &mut Self::State, message: Self::Message, - ) -> Command { + ) -> Task { self.program.update(state, message) } @@ -532,7 +532,7 @@ fn with_title( fn with_load( program: P, - f: impl Fn() -> Command, + f: impl Fn() -> Task, ) -> impl Definition { struct WithLoad { program: P, @@ -541,7 +541,7 @@ fn with_load( impl Definition for WithLoad where - F: Fn() -> Command, + F: Fn() -> Task, { type State = P::State; type Message = P::Message; @@ -549,15 +549,15 @@ fn with_load( type Renderer = P::Renderer; type Executor = executor::Default; - fn load(&self) -> Command { - Command::batch([self.program.load(), (self.load)()]) + fn load(&self) -> Task { + Task::batch([self.program.load(), (self.load)()]) } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command { + ) -> Task { self.program.update(state, message) } @@ -621,7 +621,7 @@ fn with_subscription( (self.subscription)(state) } - fn load(&self) -> Command { + fn load(&self) -> Task { self.program.load() } @@ -629,7 +629,7 @@ fn with_subscription( &self, state: &mut Self::State, message: Self::Message, - ) -> Command { + ) -> Task { self.program.update(state, message) } @@ -686,7 +686,7 @@ fn with_theme( (self.theme)(state) } - fn load(&self) -> Command { + fn load(&self) -> Task { self.program.load() } @@ -698,7 +698,7 @@ fn with_theme( &self, state: &mut Self::State, message: Self::Message, - ) -> Command { + ) -> Task { self.program.update(state, message) } @@ -755,7 +755,7 @@ fn with_style( (self.style)(state, theme) } - fn load(&self) -> Command { + fn load(&self) -> Task { self.program.load() } @@ -767,7 +767,7 @@ fn with_style( &self, state: &mut Self::State, message: Self::Message, - ) -> Command { + ) -> Task { self.program.update(state, message) } @@ -822,26 +822,26 @@ where /// The update logic of some [`Program`]. /// /// This trait allows the [`program`] builder to take any closure that -/// returns any `Into>`. +/// returns any `Into>`. pub trait Update { /// Processes the message and updates the state of the [`Program`]. fn update( &self, state: &mut State, message: Message, - ) -> impl Into>; + ) -> impl Into>; } impl Update for T where T: Fn(&mut State, Message) -> C, - C: Into>, + C: Into>, { fn update( &self, state: &mut State, message: Message, - ) -> impl Into> { + ) -> impl Into> { self(state, message) } } diff --git a/widget/src/button.rs b/widget/src/button.rs index dc949671..5d446fea 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -205,7 +205,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( diff --git a/widget/src/column.rs b/widget/src/column.rs index df7829b3..4699164c 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -208,7 +208,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/container.rs b/widget/src/container.rs index 51967707..e917471f 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -13,7 +13,7 @@ use crate::core::{ Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Command; +use crate::runtime::Task; /// An element decorating some content. /// @@ -258,7 +258,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container( self.id.as_ref().map(|id| &id.0), @@ -457,9 +457,9 @@ impl From for widget::Id { } } -/// Produces a [`Command`] that queries the visible screen bounds of the +/// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. -pub fn visible_bounds(id: Id) -> Command> { +pub fn visible_bounds(id: Id) -> Task> { struct VisibleBounds { target: widget::Id, depth: usize, @@ -538,7 +538,7 @@ pub fn visible_bounds(id: Id) -> Command> { } } - Command::widget(VisibleBounds { + Task::widget(VisibleBounds { target: id.into(), depth: 0, scrollables: Vec::new(), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 016bafbb..62343a55 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -12,7 +12,7 @@ use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; use crate::rule::{self, Rule}; -use crate::runtime::Command; +use crate::runtime::{Action, Task}; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -275,7 +275,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation, + operation: &mut dyn operation::Operation<()>, ) { self.content .as_widget() @@ -477,7 +477,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation, + operation: &mut dyn operation::Operation<()>, ) { let children = [&self.base, &self.top] .into_iter() @@ -929,19 +929,13 @@ where } /// Focuses the previous focusable widget. -pub fn focus_previous() -> Command -where - Message: 'static, -{ - Command::widget(operation::focusable::focus_previous()) +pub fn focus_previous() -> Task { + Task::effect(Action::widget(operation::focusable::focus_previous())) } /// Focuses the next focusable widget. -pub fn focus_next() -> Command -where - Message: 'static, -{ - Command::widget(operation::focusable::focus_next()) +pub fn focus_next() -> Task { + Task::effect(Action::widget(operation::focusable::focus_next())) } /// A container intercepting mouse events. diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index fdaadefa..69991d1f 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -265,7 +265,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 04783dbe..606da22d 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -182,7 +182,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { self.with_element(|element| { element.as_widget().operate( diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 7ba71a02..f079c0df 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -59,7 +59,7 @@ pub trait Component { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn widget::Operation<()>, ) { } @@ -172,7 +172,7 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); @@ -358,70 +358,17 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { self.rebuild_element_with_operation(operation); - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - bounds: Rectangle, - translation: Vector, - ) { - self.operation.scrollable(state, id, bounds, translation); - } - - fn custom( - &mut self, - state: &mut dyn std::any::Any, - id: Option<&widget::Id>, - ) { - self.operation.custom(state, id); - } - } - let tree = tree.state.downcast_mut::>>>(); self.with_element(|element| { element.as_widget().operate( &mut tree.borrow_mut().as_mut().unwrap().children[0], layout, renderer, - &mut MapOperation { operation }, + operation, ); }); } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index f612102e..27f52617 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -161,7 +161,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { let state = tree.state.downcast_mut::(); let mut content = self.content.borrow_mut(); diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index d7235cf6..17cae53b 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -178,7 +178,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { self.content.as_widget().operate( &mut tree.children[0], diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index acfa9d44..c3da3879 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -324,7 +324,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 30ad52ca..d45fc0cd 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -214,7 +214,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index c2eeebb7..c05f1252 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -278,7 +278,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation<()>, ) { let mut children = layout.children(); let padded = children.next().unwrap(); diff --git a/widget/src/row.rs b/widget/src/row.rs index fa352171..00bcf601 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -197,7 +197,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 6fc00f87..c3d08223 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -15,7 +15,7 @@ use crate::core::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Command; +use crate::runtime::{Action, Task}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; @@ -295,7 +295,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { let state = tree.state.downcast_mut::(); @@ -952,22 +952,18 @@ impl From for widget::Id { } } -/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage` along the x & y axis. -pub fn snap_to( - id: Id, - offset: RelativeOffset, -) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, offset)) +pub fn snap_to(id: Id, offset: RelativeOffset) -> Task { + Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) } -/// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`] +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// to the provided [`AbsoluteOffset`] along the x & y axis. -pub fn scroll_to( - id: Id, - offset: AbsoluteOffset, -) -> Command { - Command::widget(operation::scrollable::scroll_to(id.0, offset)) +pub fn scroll_to(id: Id, offset: AbsoluteOffset) -> Task { + Task::effect(Action::widget(operation::scrollable::scroll_to( + id.0, offset, + ))) } /// Returns [`true`] if the viewport actually changed. diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 5035541b..efa9711d 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -189,7 +189,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index dc4f83e0..4e89236b 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -30,7 +30,7 @@ use crate::core::{ Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Command; +use crate::runtime::{Action, Task}; /// A field that can be filled with text. /// @@ -540,7 +540,7 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { let state = tree.state.downcast_mut::>(); @@ -1140,35 +1140,38 @@ impl From for widget::Id { } } -/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id.0)) +/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus(id: Id) -> Task { + Task::effect(Action::widget(operation::focusable::focus(id.0))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. -pub fn move_cursor_to_end(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_end(id.0)) +pub fn move_cursor_to_end(id: Id) -> Task { + Task::effect(Action::widget(operation::text_input::move_cursor_to_end( + id.0, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. -pub fn move_cursor_to_front(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_front(id.0)) +pub fn move_cursor_to_front(id: Id) -> Task { + Task::effect(Action::widget(operation::text_input::move_cursor_to_front( + id.0, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. -pub fn move_cursor_to( - id: Id, - position: usize, -) -> Command { - Command::widget(operation::text_input::move_cursor_to(id.0, position)) +pub fn move_cursor_to(id: Id, position: usize) -> Task { + Task::effect(Action::widget(operation::text_input::move_cursor_to( + id.0, position, + ))) } -/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all(id: Id) -> Command { - Command::widget(operation::text_input::select_all(id.0)) +/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. +pub fn select_all(id: Id) -> Task { + Task::effect(Action::widget(operation::text_input::select_all(id.0))) } /// The state of a [`TextInput`]. diff --git a/widget/src/themer.rs b/widget/src/themer.rs index f4597458..9eb47d84 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -104,7 +104,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { self.content .as_widget() @@ -236,7 +236,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation, + operation: &mut dyn Operation<()>, ) { self.content.operate(layout, renderer, operation); } diff --git a/winit/src/application.rs b/winit/src/application.rs index d93ea42e..a08c2010 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -19,7 +19,7 @@ use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Command, Debug}; +use crate::runtime::{Action, Debug, Task}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; @@ -36,7 +36,7 @@ use std::sync::Arc; /// its own window. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -52,10 +52,10 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Command); + fn new(flags: Self::Flags) -> (Self, Task); /// Returns the current title of the [`Application`]. /// @@ -155,19 +155,23 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - let runtime = { + let mut runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; executor.spawn(worker); Runtime::new(executor, proxy.clone()) }; - let (application, init_command) = { + let (application, task) = { let flags = settings.flags; runtime.enter(|| A::new(flags)) }; + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } + let id = settings.id; let title = application.title(); @@ -183,7 +187,6 @@ where boot_receiver, event_receiver, control_sender, - init_command, settings.fonts, )); @@ -193,7 +196,7 @@ where instance: std::pin::Pin>, context: task::Context<'static>, boot: Option>, - sender: mpsc::UnboundedSender>, + sender: mpsc::UnboundedSender>>, receiver: mpsc::UnboundedReceiver, error: Option, #[cfg(target_arch = "wasm32")] @@ -229,7 +232,7 @@ where queued_events: Vec::new(), }; - impl winit::application::ApplicationHandler + impl winit::application::ApplicationHandler> for Runner where F: Future, @@ -393,11 +396,11 @@ where fn user_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - message: Message, + action: Action, ) { self.process_event( event_loop, - winit::event::Event::UserEvent(message), + winit::event::Event::UserEvent(action), ); } @@ -416,7 +419,7 @@ where fn process_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::Event, + event: winit::event::Event>, ) { // On Wasm, events may start being processed before the compositor // boots up. We simply queue them and process them once ready. @@ -480,15 +483,14 @@ struct Boot { async fn run_instance( mut application: A, - mut runtime: Runtime, A::Message>, + mut runtime: Runtime, Action>, mut proxy: Proxy, mut debug: Debug, mut boot: oneshot::Receiver>, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event, + winit::event::Event>, >, mut control_sender: mpsc::UnboundedSender, - init_command: Command, fonts: Vec>, ) where A: Application + 'static, @@ -518,7 +520,7 @@ async fn run_instance( let physical_size = state.physical_size(); let mut clipboard = Clipboard::connect(&window); - let mut cache = user_interface::Cache::default(); + let cache = user_interface::Cache::default(); let mut surface = compositor.create_surface( window.clone(), physical_size.width, @@ -530,22 +532,12 @@ async fn run_instance( window.set_visible(true); } - run_command( - &application, - &mut compositor, - &mut surface, - &mut cache, - &state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &window, + runtime.track( + application + .subscription() + .map(Action::Output) + .into_recipes(), ); - runtime.track(application.subscription().into_recipes()); let mut user_interface = ManuallyDrop::new(build_user_interface( &application, @@ -581,8 +573,21 @@ async fn run_instance( ), )); } - event::Event::UserEvent(message) => { - messages.push(message); + event::Event::UserEvent(action) => { + run_action( + action, + &mut user_interface, + &mut compositor, + &mut surface, + &state, + &mut renderer, + &mut messages, + &mut clipboard, + &mut should_exit, + &mut debug, + &window, + ); + user_events += 1; } event::Event::WindowEvent { @@ -756,21 +761,14 @@ async fn run_instance( user_interface::State::Outdated ) { - let mut cache = + let cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application update( &mut application, - &mut compositor, - &mut surface, - &mut cache, &mut state, - &mut renderer, &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, &mut debug, &mut messages, &window, @@ -855,282 +853,230 @@ where } /// Updates an [`Application`] by feeding it the provided messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update( +/// resulting [`Task`], and tracking its [`Subscription`]. +pub fn update( application: &mut A, - compositor: &mut C, - surface: &mut C::Surface, - cache: &mut user_interface::Cache, state: &mut State, - renderer: &mut A::Renderer, - runtime: &mut Runtime, A::Message>, - clipboard: &mut Clipboard, - should_exit: &mut bool, - proxy: &mut Proxy, + runtime: &mut Runtime, Action>, debug: &mut Debug, messages: &mut Vec, window: &winit::window::Window, ) where - C: Compositor + 'static, A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); debug.update_started(); - let command = runtime.enter(|| application.update(message)); + let task = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command( - application, - compositor, - surface, - cache, - state, - renderer, - command, - runtime, - clipboard, - should_exit, - proxy, - debug, - window, - ); + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } } state.synchronize(application, window); let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track(subscription.map(Action::Output).into_recipes()); } -/// Runs the actions of a [`Command`]. -pub fn run_command( - application: &A, +/// Runs the actions of a [`Task`]. +pub fn run_action( + action: Action, + user_interface: &mut UserInterface<'_, A::Message, A::Theme, C::Renderer>, compositor: &mut C, surface: &mut C::Surface, - cache: &mut user_interface::Cache, state: &State, renderer: &mut A::Renderer, - command: Command, - runtime: &mut Runtime, A::Message>, + messages: &mut Vec, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut Proxy, debug: &mut Debug, window: &winit::window::Window, ) where A: Application, - E: Executor, C: Compositor + 'static, A::Theme: DefaultStyle, { - use crate::runtime::command; use crate::runtime::system; use crate::runtime::window; - for action in command.actions() { - match action { - command::Action::Future(future) => { - runtime.spawn(future); + match action { + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); } - command::Action::Stream(stream) => { - runtime.run(stream); + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag, kind) => { - let message = tag(clipboard.read(kind)); - - proxy.send(message); - } - clipboard::Action::Write(contents, kind) => { - clipboard.write(kind, contents); - } - }, - command::Action::Window(action) => match action { - window::Action::Close(_id) => { - *should_exit = true; - } - window::Action::Drag(_id) => { - let _res = window.drag_window(); - } - window::Action::Spawn { .. } => { - log::warn!( - "Spawning a window is only available with \ + }, + Action::Window(action) => match action { + window::Action::Close(_id) => { + *should_exit = true; + } + window::Action::Drag(_id) => { + let _res = window.drag_window(); + } + window::Action::Open { .. } => { + log::warn!( + "Spawning a window is only available with \ multi-window applications." - ); - } - window::Action::Resize(_id, size) => { - let _ = - window.request_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); - } - window::Action::FetchSize(_id, callback) => { - let size = - window.inner_size().to_logical(window.scale_factor()); + ); + } + window::Action::Resize(_id, size) => { + let _ = window.request_inner_size(winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }); + } + window::Action::FetchSize(_id, channel) => { + let size = + window.inner_size().to_logical(window.scale_factor()); - proxy.send(callback(Size::new(size.width, size.height))); - } - window::Action::FetchMaximized(_id, callback) => { - proxy.send(callback(window.is_maximized())); - } - window::Action::Maximize(_id, maximized) => { - window.set_maximized(maximized); - } - window::Action::FetchMinimized(_id, callback) => { - proxy.send(callback(window.is_minimized())); - } - window::Action::Minimize(_id, minimized) => { - window.set_minimized(minimized); - } - window::Action::FetchPosition(_id, callback) => { - let position = window - .inner_position() - .map(|position| { - let position = position - .to_logical::(window.scale_factor()); + let _ = channel.send(Size::new(size.width, size.height)); + } + window::Action::FetchMaximized(_id, channel) => { + let _ = channel.send(window.is_maximized()); + } + window::Action::Maximize(_id, maximized) => { + window.set_maximized(maximized); + } + window::Action::FetchMinimized(_id, channel) => { + let _ = channel.send(window.is_minimized()); + } + window::Action::Minimize(_id, minimized) => { + window.set_minimized(minimized); + } + window::Action::FetchPosition(_id, channel) => { + let position = window + .inner_position() + .map(|position| { + let position = + position.to_logical::(window.scale_factor()); - Point::new(position.x, position.y) - }) - .ok(); + Point::new(position.x, position.y) + }) + .ok(); - proxy.send(callback(position)); - } - window::Action::Move(_id, position) => { - window.set_outer_position(winit::dpi::LogicalPosition { - x: position.x, - y: position.y, + let _ = channel.send(position); + } + window::Action::Move(_id, position) => { + window.set_outer_position(winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }); + } + window::Action::ChangeMode(_id, mode) => { + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.current_monitor(), + mode, + )); + } + window::Action::ChangeIcon(_id, icon) => { + window.set_window_icon(conversion::icon(icon)); + } + window::Action::FetchMode(_id, channel) => { + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); + } + window::Action::ToggleMaximize(_id) => { + window.set_maximized(!window.is_maximized()); + } + window::Action::ToggleDecorations(_id) => { + window.set_decorations(!window.is_decorated()); + } + window::Action::RequestUserAttention(_id, user_attention) => { + window.request_user_attention( + user_attention.map(conversion::user_attention), + ); + } + window::Action::GainFocus(_id) => { + window.focus_window(); + } + window::Action::ChangeLevel(_id, level) => { + window.set_window_level(conversion::window_level(level)); + } + window::Action::ShowSystemMenu(_id) => { + if let mouse::Cursor::Available(point) = state.cursor() { + window.show_window_menu(winit::dpi::LogicalPosition { + x: point.x, + y: point.y, }); } - window::Action::ChangeMode(_id, mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - mode, - )); - } - window::Action::ChangeIcon(_id, icon) => { - window.set_window_icon(conversion::icon(icon)); - } - window::Action::FetchMode(_id, tag) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - core::window::Mode::Hidden - }; + } + window::Action::FetchRawId(_id, channel) => { + let _ = channel.send(window.id().into()); + } + window::Action::RunWithHandle(_id, f) => { + use window::raw_window_handle::HasWindowHandle; - proxy.send(tag(mode)); + if let Ok(handle) = window.window_handle() { + f(handle); } - window::Action::ToggleMaximize(_id) => { - window.set_maximized(!window.is_maximized()); - } - window::Action::ToggleDecorations(_id) => { - window.set_decorations(!window.is_decorated()); - } - window::Action::RequestUserAttention(_id, user_attention) => { - window.request_user_attention( - user_attention.map(conversion::user_attention), - ); - } - window::Action::GainFocus(_id) => { - window.focus_window(); - } - window::Action::ChangeLevel(_id, level) => { - window.set_window_level(conversion::window_level(level)); - } - window::Action::ShowSystemMenu(_id) => { - if let mouse::Cursor::Available(point) = state.cursor() { - window.show_window_menu(winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }); - } - } - window::Action::FetchId(_id, tag) => { - proxy.send(tag(window.id().into())); - } - window::Action::RunWithHandle(_id, tag) => { - use window::raw_window_handle::HasWindowHandle; + } - if let Ok(handle) = window.window_handle() { - proxy.send(tag(handle)); - } - } - - window::Action::Screenshot(_id, tag) => { - let bytes = compositor.screenshot( - renderer, - surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - proxy.send(tag(window::Screenshot::new( - bytes, - state.physical_size(), - state.viewport().scale_factor(), - ))); - } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - let mut proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let message = _tag(information); - - proxy.send(message); - }); - } - } - }, - command::Action::Widget(action) => { - let mut current_cache = std::mem::take(cache); - let mut current_operation = Some(action); - - let mut user_interface = build_user_interface( - application, - current_cache, + window::Action::Screenshot(_id, channel) => { + let bytes = compositor.screenshot( renderer, - state.logical_size(), - debug, + surface, + state.viewport(), + state.background_color(), + &debug.overlay(), ); - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); + let _ = channel.send(window::Screenshot::new( + bytes, + state.physical_size(), + state.viewport().scale_factor(), + )); + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); } } + } + } + Action::LoadFont { bytes, channel } => { + // TODO: Error handling (?) + compositor.load_font(bytes); - current_cache = user_interface.into_cache(); - *cache = current_cache; - } - command::Action::LoadFont { bytes, tagger } => { - // TODO: Error handling (?) - compositor.load_font(bytes); - - proxy.send(tagger(Ok(()))); - } - command::Action::Custom(_) => { - log::warn!("Unsupported custom action in `iced_winit` shell"); - } + let _ = channel.send(Ok(())); + } + Action::Output(message) => { + messages.push(message); } } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 2eaf9241..d56b47eb 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -21,10 +21,10 @@ use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::{compositor, Compositor}; use crate::multi_window::window_manager::WindowManager; -use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; +use crate::runtime::{Action, Task}; use crate::{Clipboard, Error, Proxy, Settings}; pub use crate::application::{default, Appearance, DefaultStyle}; @@ -41,7 +41,7 @@ use std::time::Instant; /// its own window. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. +/// [`Task`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -57,10 +57,10 @@ where /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`] if you need to perform some + /// Additionally, you can return a [`Task`] if you need to perform some /// async action in the background on startup. This is useful if you want to /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Command); + fn new(flags: Self::Flags) -> (Self, Task); /// Returns the current title of the [`Application`]. /// @@ -127,19 +127,23 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - let runtime = { + let mut runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; executor.spawn(worker); Runtime::new(executor, proxy.clone()) }; - let (application, init_command) = { + let (application, task) = { let flags = settings.flags; runtime.enter(|| A::new(flags)) }; + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } + let id = settings.id; let title = application.title(window::Id::MAIN); @@ -155,7 +159,6 @@ where boot_receiver, event_receiver, control_sender, - init_command, )); let context = task::Context::from_waker(task::noop_waker_ref()); @@ -448,13 +451,12 @@ enum Control { async fn run_instance( mut application: A, - mut runtime: Runtime, A::Message>, + mut runtime: Runtime, Action>, mut proxy: Proxy, mut debug: Debug, mut boot: oneshot::Receiver>, - mut event_receiver: mpsc::UnboundedReceiver>, + mut event_receiver: mpsc::UnboundedReceiver>>, mut control_sender: mpsc::UnboundedSender, - init_command: Command, ) where A: Application + 'static, E: Executor + 'static, @@ -511,21 +513,13 @@ async fn run_instance( )]), )); - run_command( - &application, - &mut compositor, - init_command, - &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, - &mut debug, - &mut window_manager, - &mut ui_caches, + runtime.track( + application + .subscription() + .map(Action::Output) + .into_recipes(), ); - runtime.track(application.subscription().into_recipes()); - let mut messages = Vec::new(); let mut user_events = 0; @@ -594,8 +588,19 @@ async fn run_instance( ), ); } - event::Event::UserEvent(message) => { - messages.push(message); + event::Event::UserEvent(action) => { + run_action( + action, + &application, + &mut compositor, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + ); user_events += 1; } event::Event::WindowEvent { @@ -888,7 +893,7 @@ async fn run_instance( // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { - let mut cached_interfaces: FxHashMap< + let cached_interfaces: FxHashMap< window::Id, user_interface::Cache, > = ManuallyDrop::into_inner(user_interfaces) @@ -899,15 +904,9 @@ async fn run_instance( // Update application update( &mut application, - &mut compositor, &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, &mut debug, &mut messages, - &mut window_manager, - &mut cached_interfaces, ); // we must synchronize all window states with application state after an @@ -971,63 +970,46 @@ where user_interface } -/// Updates a multi-window [`Application`] by feeding it messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -fn update( +fn update( application: &mut A, - compositor: &mut C, - runtime: &mut Runtime, A::Message>, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender, - proxy: &mut Proxy, + runtime: &mut Runtime, Action>, debug: &mut Debug, messages: &mut Vec, - window_manager: &mut WindowManager, - ui_caches: &mut FxHashMap, ) where - C: Compositor + 'static, A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); debug.update_started(); - let command = runtime.enter(|| application.update(message)); + let task = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command( - application, - compositor, - command, - runtime, - clipboard, - control_sender, - proxy, - debug, - window_manager, - ui_caches, - ); + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } } let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + runtime.track(subscription.map(Action::Output).into_recipes()); } -/// Runs the actions of a [`Command`]. -fn run_command( +fn run_action( + action: Action, application: &A, compositor: &mut C, - command: Command, - runtime: &mut Runtime, A::Message>, + messages: &mut Vec, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, - proxy: &mut Proxy, debug: &mut Debug, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, A::Message, A::Theme, A::Renderer>, + >, window_manager: &mut WindowManager, ui_caches: &mut FxHashMap, ) where A: Application, - E: Executor, C: Compositor + 'static, A::Theme: DefaultStyle, { @@ -1035,279 +1017,252 @@ fn run_command( use crate::runtime::system; use crate::runtime::window; - for action in command.actions() { - match action { - command::Action::Future(future) => { - runtime.spawn(Box::pin(future)); + match action { + Action::Output(message) => { + messages.push(message); + } + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); } - command::Action::Stream(stream) => { - runtime.run(Box::pin(stream)); + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag, kind) => { - let message = tag(clipboard.read(kind)); + }, + Action::Window(action) => match action { + window::Action::Open(id, settings) => { + let monitor = window_manager.last_monitor(); - proxy.send(message); - } - clipboard::Action::Write(contents, kind) => { - clipboard.write(kind, contents); - } - }, - command::Action::Window(action) => match action { - window::Action::Spawn(id, settings) => { - let monitor = window_manager.last_monitor(); + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: application.title(id), + monitor, + }) + .expect("Send control action"); + } + window::Action::Close(id) => { + let _ = window_manager.remove(id); + let _ = ui_caches.remove(&id); + if window_manager.is_empty() { control_sender - .start_send(Control::CreateWindow { - id, - settings, - title: application.title(id), - monitor, - }) + .start_send(Control::Exit) .expect("Send control action"); } - window::Action::Close(id) => { - let _ = window_manager.remove(id); - let _ = ui_caches.remove(&id); + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + } + window::Action::FetchSize(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); - if window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); - } + let _ = channel.send(Size::new(size.width, size.height)); } - window::Action::Drag(id) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.drag_window(); - } + } + window::Action::FetchMaximized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_maximized()); } - window::Action::Resize(id, size) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.request_inner_size( - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }, - ); - } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); } - window::Action::FetchSize(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - let size = window - .raw - .inner_size() - .to_logical(window.raw.scale_factor()); + } + window::Action::FetchMinimized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_minimized()); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + window::Action::FetchPosition(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let position = window + .raw + .inner_position() + .map(|position| { + let position = position + .to_logical::(window.raw.scale_factor()); - proxy - .send(callback(Size::new(size.width, size.height))); - } - } - window::Action::FetchMaximized(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(callback(window.raw.is_maximized())); - } - } - window::Action::Maximize(id, maximized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(maximized); - } - } - window::Action::FetchMinimized(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(callback(window.raw.is_minimized())); - } - } - window::Action::Minimize(id, minimized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_minimized(minimized); - } - } - window::Action::FetchPosition(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - let position = window - .raw - .inner_position() - .map(|position| { - let position = position.to_logical::( - window.raw.scale_factor(), - ); + Point::new(position.x, position.y) + }) + .ok(); - Point::new(position.x, position.y) - }) - .ok(); - - proxy.send(callback(position)); - } + let _ = channel.send(position); } - window::Action::Move(id, position) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_outer_position( + } + window::Action::Move(id, position) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, + ); + } + } + window::Action::ChangeMode(id, mode) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); + } + } + window::Action::ChangeIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } + } + window::Action::FetchMode(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); + } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } + } + window::Action::ChangeLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( winit::dpi::LogicalPosition { - x: position.x, - y: position.y, + x: point.x, + y: point.y, }, ); } } - window::Action::ChangeMode(id, mode) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_visible(conversion::visible(mode)); - window.raw.set_fullscreen(conversion::fullscreen( - window.raw.current_monitor(), - mode, - )); - } - } - window::Action::ChangeIcon(id, icon) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_window_icon(conversion::icon(icon)); - } - } - window::Action::FetchMode(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - let mode = if window.raw.is_visible().unwrap_or(true) { - conversion::mode(window.raw.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - proxy.send(tag(mode)); - } - } - window::Action::ToggleMaximize(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(!window.raw.is_maximized()); - } - } - window::Action::ToggleDecorations(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_decorations(!window.raw.is_decorated()); - } - } - window::Action::RequestUserAttention(id, attention_type) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.request_user_attention( - attention_type.map(conversion::user_attention), - ); - } - } - window::Action::GainFocus(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.focus_window(); - } - } - window::Action::ChangeLevel(id, level) => { - if let Some(window) = window_manager.get_mut(id) { - window - .raw - .set_window_level(conversion::window_level(level)); - } - } - window::Action::ShowSystemMenu(id) => { - if let Some(window) = window_manager.get_mut(id) { - if let mouse::Cursor::Available(point) = - window.state.cursor() - { - window.raw.show_window_menu( - winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }, - ); - } - } - } - window::Action::FetchId(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(tag(window.raw.id().into())); - } - } - window::Action::RunWithHandle(id, tag) => { - use window::raw_window_handle::HasWindowHandle; - - if let Some(handle) = window_manager - .get_mut(id) - .and_then(|window| window.raw.window_handle().ok()) - { - proxy.send(tag(handle)); - } - } - window::Action::Screenshot(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - let bytes = compositor.screenshot( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - &debug.overlay(), - ); - - proxy.send(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), - window.state.viewport().scale_factor(), - ))); - } - } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - let mut proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let message = _tag(information); - - proxy.send(message); - }); - } - } - }, - command::Action::Widget(action) => { - let mut current_operation = Some(action); - - let mut uis = build_user_interfaces( - application, - debug, - window_manager, - std::mem::take(ui_caches), - ); - - while let Some(mut operation) = current_operation.take() { - for (id, ui) in uis.iter_mut() { - if let Some(window) = window_manager.get_mut(*id) { - ui.operate(&window.renderer, operation.as_mut()); - } - } - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - - *ui_caches = - uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); } - command::Action::LoadFont { bytes, tagger } => { - // TODO: Error handling (?) - compositor.load_font(bytes.clone()); + window::Action::FetchRawId(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.id().into()); + } + } + window::Action::RunWithHandle(id, f) => { + use window::raw_window_handle::HasWindowHandle; - proxy.send(tagger(Ok(()))); + if let Some(handle) = window_manager + .get_mut(id) + .and_then(|window| window.raw.window_handle().ok()) + { + f(handle); + } } - command::Action::Custom(_) => { - log::warn!("Unsupported custom action in `iced_winit` shell"); + window::Action::Screenshot(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); + + let _ = channel.send(window::Screenshot::new( + bytes, + window.state.physical_size(), + window.state.viewport().scale_factor(), + )); + } } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for (id, ui) in interfaces.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + } + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + Action::LoadFont { bytes, channel } => { + // TODO: Error handling (?) + compositor.load_font(bytes.clone()); + + let _ = channel.send(Ok(())); } } } diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 3edc30ad..0ab61375 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -4,17 +4,18 @@ use crate::futures::futures::{ task::{Context, Poll}, Future, Sink, StreamExt, }; +use crate::runtime::Action; use std::pin::Pin; /// An event loop proxy with backpressure that implements `Sink`. #[derive(Debug)] -pub struct Proxy { - raw: winit::event_loop::EventLoopProxy, - sender: mpsc::Sender, +pub struct Proxy { + raw: winit::event_loop::EventLoopProxy>, + sender: mpsc::Sender>, notifier: mpsc::Sender, } -impl Clone for Proxy { +impl Clone for Proxy { fn clone(&self) -> Self { Self { raw: self.raw.clone(), @@ -24,12 +25,12 @@ impl Clone for Proxy { } } -impl Proxy { +impl Proxy { const MAX_SIZE: usize = 100; /// Creates a new [`Proxy`] from an `EventLoopProxy`. pub fn new( - raw: winit::event_loop::EventLoopProxy, + raw: winit::event_loop::EventLoopProxy>, ) -> (Self, impl Future) { let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE); let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE); @@ -72,16 +73,16 @@ impl Proxy { ) } - /// Sends a `Message` to the event loop. + /// Sends a value to the event loop. /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send(&mut self, message: Message) + pub fn send(&mut self, value: T) where - Message: std::fmt::Debug, + T: std::fmt::Debug, { self.raw - .send_event(message) + .send_event(Action::Output(value)) .expect("Send message to event loop"); } @@ -92,7 +93,7 @@ impl Proxy { } } -impl Sink for Proxy { +impl Sink> for Proxy { type Error = mpsc::SendError; fn poll_ready( @@ -104,9 +105,9 @@ impl Sink for Proxy { fn start_send( mut self: Pin<&mut Self>, - message: Message, + action: Action, ) -> Result<(), Self::Error> { - self.sender.start_send(message) + self.sender.start_send(action) } fn poll_flush( diff --git a/winit/src/system.rs b/winit/src/system.rs index c5a5b219..7997f311 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -1,15 +1,13 @@ //! Access the native system. use crate::graphics::compositor; -use crate::runtime::command::{self, Command}; use crate::runtime::system::{Action, Information}; +use crate::runtime::{self, Task}; /// Query for available system information. -pub fn fetch_information( - f: impl Fn(Information) -> Message + Send + 'static, -) -> Command { - Command::single(command::Action::System(Action::QueryInformation( - Box::new(f), - ))) +pub fn fetch_information() -> Task { + Task::oneshot(|channel| { + runtime::Action::System(Action::QueryInformation(channel)) + }) } pub(crate) fn information( From b328da2c71e998e539bdc65815061e88dd1e7081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 01:52:30 +0200 Subject: [PATCH 040/657] Fix `Send` requirements for Wasm targets --- core/src/widget/operation.rs | 4 ++-- runtime/src/task.rs | 2 +- runtime/src/window.rs | 6 +++--- winit/src/application.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 1fa924a4..3e4ed618 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -8,7 +8,7 @@ pub use scrollable::Scrollable; pub use text_input::TextInput; use crate::widget::Id; -use crate::{MaybeSend, Rectangle, Vector}; +use crate::{Rectangle, Vector}; use std::any::Any; use std::fmt; @@ -16,7 +16,7 @@ use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. -pub trait Operation: MaybeSend { +pub trait Operation: Send { /// Operates on a widget that contains other widgets. /// /// The `operate_on_children` function can be called to return control to diff --git a/runtime/src/task.rs b/runtime/src/task.rs index ac28a4e7..f3ddbca1 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -52,7 +52,7 @@ impl Task { /// its output. pub fn widget(operation: impl widget::Operation + 'static) -> Task where - T: MaybeSend + 'static, + T: Send + 'static, { Self::channel(move |sender| { let operation = diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 0876ab69..59f285fd 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -7,7 +7,7 @@ use crate::core::time::Instant; use crate::core::window::{ Event, Icon, Id, Level, Mode, Settings, UserAttention, }; -use crate::core::{MaybeSend, Point, Size}; +use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::futures::channel::oneshot; use crate::futures::Subscription; @@ -303,10 +303,10 @@ pub fn change_icon(id: Id, icon: Icon) -> Task { /// Note that if the window closes before this call is processed the callback will not be run. pub fn run_with_handle( id: Id, - f: impl FnOnce(WindowHandle<'_>) -> T + MaybeSend + 'static, + f: impl FnOnce(WindowHandle<'_>) -> T + Send + 'static, ) -> Task where - T: MaybeSend + 'static, + T: Send + 'static, { Task::oneshot(move |channel| { crate::Action::Window(Action::RunWithHandle( diff --git a/winit/src/application.rs b/winit/src/application.rs index a08c2010..a93878ea 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -202,7 +202,7 @@ where #[cfg(target_arch = "wasm32")] is_booted: std::rc::Rc>, #[cfg(target_arch = "wasm32")] - queued_events: Vec>, + queued_events: Vec>>, } struct BootConfig { From 4ab4ffc9cfa6d7bddddb2c4d513e2120244259a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 01:54:04 +0200 Subject: [PATCH 041/657] Unpin `nightly` toolchain in `document` workflow --- .github/workflows/document.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 827a2ca8..a213e590 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: hecrj/setup-rust-action@v2 with: - rust-version: nightly-2023-12-11 + rust-version: nightly - uses: actions/checkout@v2 - name: Generate documentation run: | From 4e7cbbf98ab745351e2fb13a7c85d4ad560c21ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 01:57:49 +0200 Subject: [PATCH 042/657] Move `Maybe*` traits back to `iced_futures` --- core/src/lib.rs | 2 -- futures/src/event.rs | 2 +- futures/src/executor.rs | 3 ++- futures/src/keyboard.rs | 2 +- futures/src/lib.rs | 2 ++ {core => futures}/src/maybe.rs | 0 futures/src/runtime.rs | 3 +-- futures/src/subscription.rs | 3 +-- futures/src/subscription/tracker.rs | 3 +-- graphics/src/compositor.rs | 3 ++- runtime/src/task.rs | 3 +-- 11 files changed, 12 insertions(+), 14 deletions(-) rename {core => futures}/src/maybe.rs (100%) diff --git a/core/src/lib.rs b/core/src/lib.rs index db67219c..32156441 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -35,7 +35,6 @@ mod color; mod content_fit; mod element; mod length; -mod maybe; mod padding; mod pixels; mod point; @@ -60,7 +59,6 @@ pub use font::Font; pub use gradient::Gradient; pub use layout::Layout; pub use length::Length; -pub use maybe::{MaybeSend, MaybeSync}; pub use overlay::Overlay; pub use padding::Padding; pub use pixels::Pixels; diff --git a/futures/src/event.rs b/futures/src/event.rs index ab895fcd..72ea78ad 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -1,8 +1,8 @@ //! Listen to runtime events. use crate::core::event::{self, Event}; use crate::core::window; -use crate::core::MaybeSend; use crate::subscription::{self, Subscription}; +use crate::MaybeSend; /// Returns a [`Subscription`] to all the ignored runtime events. /// diff --git a/futures/src/executor.rs b/futures/src/executor.rs index a9dde465..3b0d4af1 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -1,5 +1,6 @@ //! Choose your preferred executor to power a runtime. -use crate::core::MaybeSend; +use crate::MaybeSend; + use futures::Future; /// A type that can run futures. diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index c86e2169..f0d7d757 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -2,8 +2,8 @@ use crate::core; use crate::core::event; use crate::core::keyboard::{Event, Key, Modifiers}; -use crate::core::MaybeSend; use crate::subscription::{self, Subscription}; +use crate::MaybeSend; /// Listens to keyboard key presses and calls the given function /// map them into actual messages. diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 01b56306..a874a618 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -8,6 +8,7 @@ pub use futures; pub use iced_core as core; +mod maybe; mod runtime; pub mod backend; @@ -17,6 +18,7 @@ pub mod keyboard; pub mod subscription; pub use executor::Executor; +pub use maybe::{MaybeSend, MaybeSync}; pub use platform::*; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/core/src/maybe.rs b/futures/src/maybe.rs similarity index 100% rename from core/src/maybe.rs rename to futures/src/maybe.rs diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 045fde6c..157e2c67 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,7 +1,6 @@ //! Run commands and keep track of subscriptions. -use crate::core::MaybeSend; use crate::subscription; -use crate::{BoxFuture, BoxStream, Executor}; +use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; use futures::{channel::mpsc, Sink}; use std::marker::PhantomData; diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 85a8a787..316fc44d 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -5,9 +5,8 @@ pub use tracker::Tracker; use crate::core::event; use crate::core::window; -use crate::core::MaybeSend; use crate::futures::{Future, Stream}; -use crate::BoxStream; +use crate::{BoxStream, MaybeSend}; use futures::channel::mpsc; use futures::never::Never; diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index c5a7bb99..f17e3ea3 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,6 +1,5 @@ -use crate::core::MaybeSend; use crate::subscription::{Event, Hasher, Recipe}; -use crate::BoxFuture; +use crate::{BoxFuture, MaybeSend}; use futures::channel::mpsc; use futures::sink::{Sink, SinkExt}; diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 4ac90f92..47521eb0 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -1,6 +1,7 @@ //! A compositor is responsible for initializing a renderer and managing window //! surfaces. -use crate::core::{Color, MaybeSend, MaybeSync}; +use crate::core::Color; +use crate::futures::{MaybeSend, MaybeSync}; use crate::{Error, Settings, Viewport}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; diff --git a/runtime/src/task.rs b/runtime/src/task.rs index f3ddbca1..db3c0674 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -1,11 +1,10 @@ use crate::core::widget; -use crate::core::MaybeSend; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; use crate::futures::futures::future::{self, FutureExt}; use crate::futures::futures::never::Never; use crate::futures::futures::stream::{self, Stream, StreamExt}; -use crate::futures::{boxed_stream, BoxStream}; +use crate::futures::{boxed_stream, BoxStream, MaybeSend}; use crate::Action; use std::future::Future; From b21e4567dc32250c90d2ea9c78080cd8bcb66368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 02:23:25 +0200 Subject: [PATCH 043/657] Remove `parent` from `PlatformSpecific` window settings --- core/Cargo.toml | 3 --- core/src/window/settings/windows.rs | 5 ----- winit/src/conversion.rs | 6 +----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 3c557bca..a1228909 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -33,8 +33,5 @@ web-time.workspace = true dark-light.workspace = true dark-light.optional = true -[target.'cfg(windows)'.dependencies] -raw-window-handle.workspace = true - [dev-dependencies] approx = "0.5" diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs index d3bda259..88fe2fbd 100644 --- a/core/src/window/settings/windows.rs +++ b/core/src/window/settings/windows.rs @@ -1,12 +1,8 @@ //! Platform specific settings for Windows. -use raw_window_handle::RawWindowHandle; /// The platform specific window settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { - /// Parent window - pub parent: Option, - /// Drag and drop support pub drag_and_drop: bool, @@ -17,7 +13,6 @@ pub struct PlatformSpecific { impl Default for PlatformSpecific { fn default() -> Self { Self { - parent: None, drag_and_drop: true, skip_taskbar: false, } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 79fcf92e..0ed10c88 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -73,11 +73,7 @@ pub fn window_attributes( #[cfg(target_os = "windows")] { use winit::platform::windows::WindowAttributesExtWindows; - #[allow(unsafe_code)] - unsafe { - attributes = attributes - .with_parent_window(settings.platform_specific.parent); - } + attributes = attributes .with_drag_and_drop(settings.platform_specific.drag_and_drop); From 88b938440285fdb44c9e5bd572fda5c0f94996ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 03:04:51 +0200 Subject: [PATCH 044/657] Use `Task` chaining to simplify `multi_window` example --- examples/multi_window/src/main.rs | 155 ++++++++++++++---------------- examples/screenshot/src/main.rs | 6 +- runtime/src/window.rs | 38 ++++++-- winit/src/multi_window.rs | 4 +- 4 files changed, 107 insertions(+), 96 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index e15f8759..fa9adb87 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,16 +1,15 @@ -use iced::event; use iced::executor; use iced::multi_window::{self, Application}; use iced::widget::{ - button, center, column, container, scrollable, text, text_input, + button, center, column, container, horizontal_space, scrollable, text, + text_input, }; use iced::window; use iced::{ - Alignment, Element, Length, Point, Settings, Subscription, Task, Theme, - Vector, + Alignment, Element, Length, Settings, Subscription, Task, Theme, Vector, }; -use std::collections::HashMap; +use std::collections::BTreeMap; fn main() -> iced::Result { Example::run(Settings::default()) @@ -18,8 +17,7 @@ fn main() -> iced::Result { #[derive(Default)] struct Example { - windows: HashMap, - next_window_pos: window::Position, + windows: BTreeMap, } #[derive(Debug)] @@ -33,13 +31,12 @@ struct Window { #[derive(Debug, Clone)] enum Message { + OpenWindow, + WindowOpened(window::Id), + WindowClosed(window::Id), ScaleInputChanged(window::Id, String), ScaleChanged(window::Id, String), TitleChanged(window::Id, String), - CloseWindow(window::Id), - WindowOpened(window::Id, Option), - WindowClosed(window::Id), - NewWindow, } impl multi_window::Application for Example { @@ -51,8 +48,7 @@ impl multi_window::Application for Example { fn new(_flags: ()) -> (Self, Task) { ( Example { - windows: HashMap::from([(window::Id::MAIN, Window::new(1))]), - next_window_pos: window::Position::Default, + windows: BTreeMap::from([(window::Id::MAIN, Window::new(1))]), }, Task::none(), ) @@ -62,48 +58,36 @@ impl multi_window::Application for Example { self.windows .get(&window) .map(|window| window.title.clone()) - .unwrap_or("Example".to_string()) + .unwrap_or_default() } fn update(&mut self, message: Message) -> Task { match message { - Message::ScaleInputChanged(id, scale) => { - let window = - self.windows.get_mut(&id).expect("Window not found!"); - window.scale_input = scale; + Message::OpenWindow => { + let Some(last_window) = self.windows.keys().last() else { + return Task::none(); + }; - Task::none() + window::fetch_position(*last_window) + .then(|last_position| { + let position = last_position.map_or( + window::Position::Default, + |last_position| { + window::Position::Specific( + last_position + Vector::new(20.0, 20.0), + ) + }, + ); + + window::open(window::Settings { + position, + ..window::Settings::default() + }) + }) + .map(Message::WindowOpened) } - Message::ScaleChanged(id, scale) => { - let window = - self.windows.get_mut(&id).expect("Window not found!"); - - window.current_scale = scale - .parse::() - .unwrap_or(window.current_scale) - .clamp(0.5, 5.0); - - Task::none() - } - Message::TitleChanged(id, title) => { - let window = - self.windows.get_mut(&id).expect("Window not found."); - - window.title = title; - - Task::none() - } - Message::CloseWindow(id) => window::close(id), - Message::WindowClosed(id) => { - self.windows.remove(&id); - Task::none() - } - Message::WindowOpened(id, position) => { - if let Some(position) = position { - self.next_window_pos = window::Position::Specific( - position + Vector::new(20.0, 20.0), - ); - } + Message::WindowOpened(id) => { + self.windows.insert(id, Window::new(self.windows.len() + 1)); if let Some(window) = self.windows.get(&id) { text_input::focus(window.input_id.clone()) @@ -111,30 +95,52 @@ impl multi_window::Application for Example { Task::none() } } - Message::NewWindow => { - let count = self.windows.len() + 1; + Message::WindowClosed(id) => { + self.windows.remove(&id); - let (id, spawn_window) = window::open(window::Settings { - position: self.next_window_pos, - exit_on_close_request: count % 2 == 0, - ..Default::default() - }); + Task::none() + } + Message::ScaleInputChanged(id, scale) => { + if let Some(window) = self.windows.get_mut(&id) { + window.scale_input = scale; + } - self.windows.insert(id, Window::new(count)); + Task::none() + } + Message::ScaleChanged(id, scale) => { + if let Some(window) = self.windows.get_mut(&id) { + window.current_scale = scale + .parse::() + .unwrap_or(window.current_scale) + .clamp(0.5, 5.0); + } - spawn_window + Task::none() + } + Message::TitleChanged(id, title) => { + if let Some(window) = self.windows.get_mut(&id) { + window.title = title; + } + + Task::none() } } } - fn view(&self, window: window::Id) -> Element { - let content = self.windows.get(&window).unwrap().view(window); - - center(content).into() + fn view(&self, window_id: window::Id) -> Element { + if let Some(window) = self.windows.get(&window_id) { + center(window.view(window_id)).into() + } else { + horizontal_space().into() + } } - fn theme(&self, window: window::Id) -> Self::Theme { - self.windows.get(&window).unwrap().theme.clone() + fn theme(&self, window: window::Id) -> Theme { + if let Some(window) = self.windows.get(&window) { + window.theme.clone() + } else { + Theme::default() + } } fn scale_factor(&self, window: window::Id) -> f64 { @@ -145,24 +151,7 @@ impl multi_window::Application for Example { } fn subscription(&self) -> Subscription { - event::listen_with(|event, _, window| { - if let iced::Event::Window(window_event) = event { - match window_event { - window::Event::CloseRequested => { - Some(Message::CloseWindow(window)) - } - window::Event::Opened { position, .. } => { - Some(Message::WindowOpened(window, position)) - } - window::Event::Closed => { - Some(Message::WindowClosed(window)) - } - _ => None, - } - } else { - None - } - }) + window::closings().map(Message::WindowClosed) } } @@ -200,7 +189,7 @@ impl Window { ]; let new_window_button = - button(text("New Window")).on_press(Message::NewWindow); + button(text("New Window")).on_press(Message::OpenWindow); let content = scrollable( column![scale_input, title_input, new_window_button] diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 9b9162d0..78d3e9ff 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -34,7 +34,7 @@ struct Example { enum Message { Crop, Screenshot, - ScreenshotData(Screenshot), + Screenshotted(Screenshot), Png, PngSaved(Result), XInputChanged(Option), @@ -48,9 +48,9 @@ impl Example { match message { Message::Screenshot => { return iced::window::screenshot(window::Id::MAIN) - .map(Message::ScreenshotData); + .map(Message::Screenshotted); } - Message::ScreenshotData(screenshot) => { + Message::Screenshotted(screenshot) => { self.screenshot = Some(screenshot); } Message::Png => { diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 59f285fd..2ba3e796 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -21,7 +21,7 @@ use raw_window_handle::WindowHandle; #[allow(missing_debug_implementations)] pub enum Action { /// Opens a new window with some [`Settings`]. - Open(Id, Settings), + Open(Id, Settings, oneshot::Sender), /// Close the window and exits the application. Close(Id), @@ -155,16 +155,36 @@ pub fn frames() -> Subscription { }) } -/// Opens a new window with the given `settings`. -/// -/// Returns the new window [`Id`] alongside the [`Task`]. -pub fn open(settings: Settings) -> (Id, Task) { +/// Subscribes to all window close requests of the running application. +pub fn close_requests() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::CloseRequested) = event { + Some(id) + } else { + None + } + }) +} + +/// Subscribes to all window closings of the running application. +pub fn closings() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Closed) = event { + Some(id) + } else { + None + } + }) +} + +/// Opens a new window with the given [`Settings`]; producing the [`Id`] +/// of the new window on completion. +pub fn open(settings: Settings) -> Task { let id = Id::unique(); - ( - id, - Task::effect(crate::Action::Window(Action::Open(id, settings))), - ) + Task::oneshot(|channel| { + crate::Action::Window(Action::Open(id, settings, channel)) + }) } /// Closes the window with `id`. diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index d56b47eb..8bd8a64d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1030,7 +1030,7 @@ fn run_action( } }, Action::Window(action) => match action { - window::Action::Open(id, settings) => { + window::Action::Open(id, settings, channel) => { let monitor = window_manager.last_monitor(); control_sender @@ -1041,6 +1041,8 @@ fn run_action( monitor, }) .expect("Send control action"); + + let _ = channel.send(id); } window::Action::Close(id) => { let _ = window_manager.remove(id); From 20945e3f9013c663deeb71096c749bc7b90d462c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 03:11:07 +0200 Subject: [PATCH 045/657] Simplify `WindowOpened` message handler in `multi_window` example --- examples/multi_window/src/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index fa9adb87..14e4e56b 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -87,13 +87,12 @@ impl multi_window::Application for Example { .map(Message::WindowOpened) } Message::WindowOpened(id) => { - self.windows.insert(id, Window::new(self.windows.len() + 1)); + let window = Window::new(self.windows.len() + 1); + let focus_input = text_input::focus(window.input_id.clone()); - if let Some(window) = self.windows.get(&id) { - text_input::focus(window.input_id.clone()) - } else { - Task::none() - } + self.windows.insert(id, window); + + focus_input } Message::WindowClosed(id) => { self.windows.remove(&id); From 7f13fab0582fe681b8546126245512bdc2338ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 14 Jun 2024 03:15:14 +0200 Subject: [PATCH 046/657] Use all themes in `multi_window` example --- examples/multi_window/src/main.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 14e4e56b..ba764654 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -160,11 +160,7 @@ impl Window { title: format!("Window_{}", count), scale_input: "1.0".to_string(), current_scale: 1.0, - theme: if count % 2 == 0 { - Theme::Light - } else { - Theme::Dark - }, + theme: Theme::ALL[count % Theme::ALL.len()].clone(), input_id: text_input::Id::unique(), } } From 43033c7f83b13838803bc82a7bbef654a8071892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 15 Jun 2024 00:43:51 +0200 Subject: [PATCH 047/657] Implement `Task::collect` --- runtime/src/task.rs | 86 +++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index db3c0674..51cdf5a8 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -47,6 +47,42 @@ impl Task { Self(Some(boxed_stream(stream.map(Action::Output)))) } + /// Creates a [`Task`] that runs the given [`Future`] to completion and maps its + /// output with the given closure. + pub fn perform( + future: impl Future + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: MaybeSend + 'static, + A: MaybeSend + 'static, + { + Self::future(future.map(f)) + } + + /// Creates a [`Task`] that runs the given [`Stream`] to completion and maps each + /// item with the given closure. + pub fn run( + stream: impl Stream + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: 'static, + { + Self::stream(stream.map(f)) + } + + /// Combines the given tasks and produces a single [`Task`] that will run all of them + /// in parallel. + pub fn batch(tasks: impl IntoIterator) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream::select_all( + tasks.into_iter().filter_map(|task| task.0), + )))) + } + /// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces /// its output. pub fn widget(operation: impl widget::Operation + 'static) -> Task @@ -163,38 +199,34 @@ impl Task { } } - /// Creates a [`Task`] that runs the given [`Future`] to completion. - pub fn perform( - future: impl Future + MaybeSend + 'static, - f: impl Fn(A) -> T + MaybeSend + 'static, - ) -> Self + /// Creates a new [`Task`] that collects all the output of the current one into a [`Vec`]. + pub fn collect(self) -> Task> where T: MaybeSend + 'static, - A: MaybeSend + 'static, { - Self::future(future.map(f)) - } + match self.0 { + None => Task::done(Vec::new()), + Some(stream) => Task(Some(boxed_stream( + stream::unfold( + (stream, Vec::new()), + |(mut stream, mut outputs)| async move { + let action = stream.next().await?; - /// Creates a [`Task`] that runs the given [`Stream`] to completion. - pub fn run( - stream: impl Stream + MaybeSend + 'static, - f: impl Fn(A) -> T + 'static + MaybeSend, - ) -> Self - where - T: 'static, - { - Self::stream(stream.map(f)) - } + match action.output() { + Ok(output) => { + outputs.push(output); - /// Combines the given tasks and produces a single [`Task`] that will run all of them - /// in parallel. - pub fn batch(tasks: impl IntoIterator) -> Self - where - T: 'static, - { - Self(Some(boxed_stream(stream::select_all( - tasks.into_iter().filter_map(|task| task.0), - )))) + Some((None, (stream, outputs))) + } + Err(action) => { + Some((Some(action), (stream, outputs))) + } + } + }, + ) + .filter_map(future::ready), + ))), + } } /// Returns the underlying [`Stream`] of the [`Task`]. From ad2e4c535af01453777e330aa828db6988f9c4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 15 Jun 2024 01:16:04 +0200 Subject: [PATCH 048/657] Fix `Task::collect` not producing the collected outputs --- runtime/src/task.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 51cdf5a8..740360ac 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -208,18 +208,25 @@ impl Task { None => Task::done(Vec::new()), Some(stream) => Task(Some(boxed_stream( stream::unfold( - (stream, Vec::new()), - |(mut stream, mut outputs)| async move { - let action = stream.next().await?; + (stream, Some(Vec::new())), + move |(mut stream, outputs)| async move { + let mut outputs = outputs?; + + let Some(action) = stream.next().await else { + return Some(( + Some(Action::Output(outputs)), + (stream, None), + )); + }; match action.output() { Ok(output) => { outputs.push(output); - Some((None, (stream, outputs))) + Some((None, (stream, Some(outputs)))) } Err(action) => { - Some((Some(action), (stream, outputs))) + Some((Some(action), (stream, Some(outputs)))) } } }, From b8321b8b2ba6776c557ee65e074ade6525e00280 Mon Sep 17 00:00:00 2001 From: henrispriet <36509362+henrispriet@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:36:48 +0000 Subject: [PATCH 049/657] Fix DEPENDENCIES.md for wayland --- DEPENDENCIES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 5d738d85..ffb4256b 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -25,6 +25,10 @@ pkgs.mkShell rec { xorg.libXcursor xorg.libXi xorg.libXrandr + + wayland + wayland.dev + libxkbcommon ]; LD_LIBRARY_PATH = From a892135206d258805c619b97d834658465bfcc5c Mon Sep 17 00:00:00 2001 From: henrispriet <36509362+henrispriet@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:42:21 +0000 Subject: [PATCH 050/657] actually wayland.dev isn't needed --- DEPENDENCIES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index ffb4256b..87fd8c7c 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -25,9 +25,7 @@ pkgs.mkShell rec { xorg.libXcursor xorg.libXi xorg.libXrandr - wayland - wayland.dev libxkbcommon ]; From b5c5a016c4f2b608a740b37c494186557a064f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 16 Jun 2024 20:15:55 +0200 Subject: [PATCH 051/657] Rename `window::closings` to `window::close_events` --- examples/multi_window/src/main.rs | 2 +- runtime/src/window.rs | 32 ++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index ba764654..b82ad1f3 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -150,7 +150,7 @@ impl multi_window::Application for Example { } fn subscription(&self) -> Subscription { - window::closings().map(Message::WindowClosed) + window::close_events().map(Message::WindowClosed) } } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 2ba3e796..956a20e1 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -155,10 +155,21 @@ pub fn frames() -> Subscription { }) } -/// Subscribes to all window close requests of the running application. -pub fn close_requests() -> Subscription { +/// Subscribes to all window events of the running application. +pub fn events() -> Subscription<(Id, Event)> { event::listen_with(|event, _status, id| { - if let crate::core::Event::Window(Event::CloseRequested) = event { + if let crate::core::Event::Window(event) = event { + Some((id, event)) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::Closed`] occurrences in the running application. +pub fn open_events() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Closed) = event { Some(id) } else { None @@ -166,8 +177,8 @@ pub fn close_requests() -> Subscription { }) } -/// Subscribes to all window closings of the running application. -pub fn closings() -> Subscription { +/// Subscribes to all [`Event::Closed`] occurrences in the running application. +pub fn close_events() -> Subscription { event::listen_with(|event, _status, id| { if let crate::core::Event::Window(Event::Closed) = event { Some(id) @@ -177,6 +188,17 @@ pub fn closings() -> Subscription { }) } +/// Subscribes to all [`Event::CloseRequested`] occurences in the running application. +pub fn close_requests() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::CloseRequested) = event { + Some(id) + } else { + None + } + }) +} + /// Opens a new window with the given [`Settings`]; producing the [`Id`] /// of the new window on completion. pub fn open(settings: Settings) -> Task { From 681765110bdc862c5d5479f9d928ec7f04f90155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 16 Jun 2024 22:02:25 +0200 Subject: [PATCH 052/657] Make crazy users acknowledge they are crazy when filing bug reports in bad faith --- .github/ISSUE_TEMPLATE/BUG-REPORT.yml | 18 ++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 20ef2b73..5053a2fa 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -6,6 +6,24 @@ body: attributes: value: | Thanks for taking the time to fill out this bug report! + - type: checkboxes + attributes: + label: Is your issue REALLY a bug? + description: | + This issue tracker is for __BUG REPORTS ONLY__. + + It's obvious, right? This is a bug report form, after all! Still, some crazy users seem to forcefully fill + out this form just to ask questions and request features. + + The core team does not appreciate that. Don't do it. + + If you want to ask a question or request a feature, please [go back here](https://github.com/iced-rs/iced/issues/new/choose) + and read carefully. + options: + - label: My issue is indeed a bug! + required: true + - label: I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise. + required: true - type: checkboxes attributes: label: Is there an existing issue for this? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5177386c..3e2486d3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,7 +3,7 @@ contact_links: - name: I have a question url: https://discourse.iced.rs/c/learn/6 about: Ask and learn from others in the Discourse forum. - - name: I want to start a discussion + - name: I want to request a feature or start a discussion url: https://discourse.iced.rs/c/request-feedback/7 about: Share your idea and gather feedback in the Discourse forum. - name: I want to chat with other users of the library From 64426b729854ef075bfb2dc143c6519faabdd36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 16 Jun 2024 22:04:05 +0200 Subject: [PATCH 053/657] Fix `BUG_REPORT` formatting --- .github/ISSUE_TEMPLATE/BUG-REPORT.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 5053a2fa..79441958 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -12,13 +12,11 @@ body: description: | This issue tracker is for __BUG REPORTS ONLY__. - It's obvious, right? This is a bug report form, after all! Still, some crazy users seem to forcefully fill - out this form just to ask questions and request features. + It's obvious, right? This is a bug report form, after all! Still, some crazy users seem to forcefully fill out this form just to ask questions and request features. The core team does not appreciate that. Don't do it. - If you want to ask a question or request a feature, please [go back here](https://github.com/iced-rs/iced/issues/new/choose) - and read carefully. + If you want to ask a question or request a feature, please [go back here](https://github.com/iced-rs/iced/issues/new/choose) and read carefully. options: - label: My issue is indeed a bug! required: true From e0b4ddf7b725ad584399ebf19a6c624a3bfcfe10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 16 Jun 2024 23:13:20 +0200 Subject: [PATCH 054/657] Flatten state in `tour` example --- examples/tour/src/main.rs | 624 +++++++++++++++++--------------------- 1 file changed, 273 insertions(+), 351 deletions(-) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index f624053c..6b10ec75 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -21,190 +21,27 @@ pub fn main() -> iced::Result { .run() } -#[derive(Default)] pub struct Tour { - steps: Steps, + step: Step, + slider: u8, + layout: Layout, + spacing: u16, + text_size: u16, + text_color: Color, + language: Option, + toggler: bool, + image_width: u16, + image_filter_method: image::FilterMethod, + input_value: String, + input_is_secure: bool, + input_is_showing_icon: bool, debug: bool, } -impl Tour { - fn title(&self) -> String { - format!("{} - Iced", self.steps.title()) - } - - fn update(&mut self, event: Message) { - match event { - Message::BackPressed => { - self.steps.go_back(); - } - Message::NextPressed => { - self.steps.advance(); - } - Message::StepMessage(step_msg) => { - self.steps.update(step_msg, &mut self.debug); - } - } - } - - fn view(&self) -> Element { - let Tour { steps, .. } = self; - - let controls = - row![] - .push_maybe(steps.has_previous().then(|| { - padded_button("Back") - .on_press(Message::BackPressed) - .style(button::secondary) - })) - .push(horizontal_space()) - .push_maybe(steps.can_continue().then(|| { - padded_button("Next").on_press(Message::NextPressed) - })); - - let content: Element<_> = column![ - steps.view(self.debug).map(Message::StepMessage), - controls, - ] - .max_width(540) - .spacing(20) - .padding(20) - .into(); - - let scrollable = scrollable( - container(if self.debug { - content.explain(Color::BLACK) - } else { - content - }) - .center_x(Length::Fill), - ); - - container(scrollable).center_y(Length::Fill).into() - } -} - #[derive(Debug, Clone)] pub enum Message { BackPressed, NextPressed, - StepMessage(StepMessage), -} - -struct Steps { - steps: Vec, - current: usize, -} - -impl Steps { - fn new() -> Steps { - Steps { - steps: vec![ - Step::Welcome, - Step::Slider { value: 50 }, - Step::RowsAndColumns { - layout: Layout::Row, - spacing: 20, - }, - Step::Text { - size: 30, - color: Color::BLACK, - }, - Step::Radio { selection: None }, - Step::Toggler { - can_continue: false, - }, - Step::Image { - width: 300, - filter_method: image::FilterMethod::Linear, - }, - Step::Scrollable, - Step::TextInput { - value: String::new(), - is_secure: false, - is_showing_icon: false, - }, - Step::Debugger, - Step::End, - ], - current: 0, - } - } - - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - self.steps[self.current].update(msg, debug); - } - - fn view(&self, debug: bool) -> Element { - self.steps[self.current].view(debug) - } - - fn advance(&mut self) { - if self.can_continue() { - self.current += 1; - } - } - - fn go_back(&mut self) { - if self.has_previous() { - self.current -= 1; - } - } - - fn has_previous(&self) -> bool { - self.current > 0 - } - - fn can_continue(&self) -> bool { - self.current + 1 < self.steps.len() - && self.steps[self.current].can_continue() - } - - fn title(&self) -> &str { - self.steps[self.current].title() - } -} - -impl Default for Steps { - fn default() -> Self { - Steps::new() - } -} - -enum Step { - Welcome, - Slider { - value: u8, - }, - RowsAndColumns { - layout: Layout, - spacing: u16, - }, - Text { - size: u16, - color: Color, - }, - Radio { - selection: Option, - }, - Toggler { - can_continue: bool, - }, - Image { - width: u16, - filter_method: image::FilterMethod, - }, - Scrollable, - TextInput { - value: String, - is_secure: bool, - is_showing_icon: bool, - }, - Debugger, - End, -} - -#[derive(Debug, Clone)] -pub enum StepMessage { SliderChanged(u8), LayoutChanged(Layout), SpacingChanged(u16), @@ -220,147 +57,145 @@ pub enum StepMessage { TogglerChanged(bool), } -impl<'a> Step { - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - match msg { - StepMessage::DebugToggled(value) => { - if let Step::Debugger = self { - *debug = value; - } - } - StepMessage::LanguageSelected(language) => { - if let Step::Radio { selection } = self { - *selection = Some(language); - } - } - StepMessage::SliderChanged(new_value) => { - if let Step::Slider { value, .. } = self { - *value = new_value; - } - } - StepMessage::TextSizeChanged(new_size) => { - if let Step::Text { size, .. } = self { - *size = new_size; - } - } - StepMessage::TextColorChanged(new_color) => { - if let Step::Text { color, .. } = self { - *color = new_color; - } - } - StepMessage::LayoutChanged(new_layout) => { - if let Step::RowsAndColumns { layout, .. } = self { - *layout = new_layout; - } - } - StepMessage::SpacingChanged(new_spacing) => { - if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing; - } - } - StepMessage::ImageWidthChanged(new_width) => { - if let Step::Image { width, .. } = self { - *width = new_width; - } - } - StepMessage::ImageUseNearestToggled(use_nearest) => { - if let Step::Image { filter_method, .. } = self { - *filter_method = if use_nearest { - image::FilterMethod::Nearest - } else { - image::FilterMethod::Linear - }; - } - } - StepMessage::InputChanged(new_value) => { - if let Step::TextInput { value, .. } = self { - *value = new_value; - } - } - StepMessage::ToggleSecureInput(toggle) => { - if let Step::TextInput { is_secure, .. } = self { - *is_secure = toggle; - } - } - StepMessage::TogglerChanged(value) => { - if let Step::Toggler { can_continue, .. } = self { - *can_continue = value; - } - } - StepMessage::ToggleTextInputIcon(toggle) => { - if let Step::TextInput { - is_showing_icon, .. - } = self - { - *is_showing_icon = toggle; - } - } - }; - } - - fn title(&self) -> &str { - match self { +impl Tour { + fn title(&self) -> String { + let screen = match self.step { Step::Welcome => "Welcome", - Step::Radio { .. } => "Radio button", - Step::Toggler { .. } => "Toggler", - Step::Slider { .. } => "Slider", - Step::Text { .. } => "Text", - Step::Image { .. } => "Image", - Step::RowsAndColumns { .. } => "Rows and columns", + Step::Radio => "Radio button", + Step::Toggler => "Toggler", + Step::Slider => "Slider", + Step::Text => "Text", + Step::Image => "Image", + Step::RowsAndColumns => "Rows and columns", Step::Scrollable => "Scrollable", - Step::TextInput { .. } => "Text input", + Step::TextInput => "Text input", Step::Debugger => "Debugger", Step::End => "End", + }; + + format!("{} - Iced", screen) + } + + fn update(&mut self, event: Message) { + match event { + Message::BackPressed => { + if let Some(step) = self.step.previous() { + self.step = step; + } + } + Message::NextPressed => { + if let Some(step) = self.step.next() { + self.step = step; + } + } + Message::SliderChanged(value) => { + self.slider = value; + } + Message::LayoutChanged(layout) => { + self.layout = layout; + } + Message::SpacingChanged(spacing) => { + self.spacing = spacing; + } + Message::TextSizeChanged(text_size) => { + self.text_size = text_size; + } + Message::TextColorChanged(text_color) => { + self.text_color = text_color; + } + Message::LanguageSelected(language) => { + self.language = Some(language); + } + Message::ImageWidthChanged(image_width) => { + self.image_width = image_width; + } + Message::ImageUseNearestToggled(use_nearest) => { + self.image_filter_method = if use_nearest { + image::FilterMethod::Nearest + } else { + image::FilterMethod::Linear + }; + } + Message::InputChanged(input_value) => { + self.input_value = input_value; + } + Message::ToggleSecureInput(is_secure) => { + self.input_is_secure = is_secure; + } + Message::ToggleTextInputIcon(show_icon) => { + self.input_is_showing_icon = show_icon; + } + Message::DebugToggled(debug) => { + self.debug = debug; + } + Message::TogglerChanged(toggler) => { + self.toggler = toggler; + } } } + fn view(&self) -> Element { + let controls = + row![] + .push_maybe(self.step.previous().is_some().then(|| { + padded_button("Back") + .on_press(Message::BackPressed) + .style(button::secondary) + })) + .push(horizontal_space()) + .push_maybe(self.can_continue().then(|| { + padded_button("Next").on_press(Message::NextPressed) + })); + + let screen = match self.step { + Step::Welcome => self.welcome(), + Step::Radio => self.radio(), + Step::Toggler => self.toggler(), + Step::Slider => self.slider(), + Step::Text => self.text(), + Step::Image => self.image(), + Step::RowsAndColumns => self.rows_and_columns(), + Step::Scrollable => self.scrollable(), + Step::TextInput => self.text_input(), + Step::Debugger => self.debugger(), + Step::End => self.end(), + }; + + let content: Element<_> = column![screen, controls,] + .max_width(540) + .spacing(20) + .padding(20) + .into(); + + let scrollable = scrollable( + container(if self.debug { + content.explain(Color::BLACK) + } else { + content + }) + .center_x(Length::Fill), + ); + + container(scrollable).center_y(Length::Fill).into() + } + fn can_continue(&self) -> bool { - match self { + match self.step { Step::Welcome => true, - Step::Radio { selection } => *selection == Some(Language::Rust), - Step::Toggler { can_continue } => *can_continue, - Step::Slider { .. } => true, - Step::Text { .. } => true, - Step::Image { .. } => true, - Step::RowsAndColumns { .. } => true, + Step::Radio => self.language == Some(Language::Rust), + Step::Toggler => self.toggler, + Step::Slider => true, + Step::Text => true, + Step::Image => true, + Step::RowsAndColumns => true, Step::Scrollable => true, - Step::TextInput { value, .. } => !value.is_empty(), + Step::TextInput => !self.input_value.is_empty(), Step::Debugger => true, Step::End => false, } } - fn view(&self, debug: bool) -> Element { - match self { - Step::Welcome => Self::welcome(), - Step::Radio { selection } => Self::radio(*selection), - Step::Toggler { can_continue } => Self::toggler(*can_continue), - Step::Slider { value } => Self::slider(*value), - Step::Text { size, color } => Self::text(*size, *color), - Step::Image { - width, - filter_method, - } => Self::image(*width, *filter_method), - Step::RowsAndColumns { layout, spacing } => { - Self::rows_and_columns(*layout, *spacing) - } - Step::Scrollable => Self::scrollable(), - Step::TextInput { - value, - is_secure, - is_showing_icon, - } => Self::text_input(value, *is_secure, *is_showing_icon), - Step::Debugger => Self::debugger(debug), - Step::End => Self::end(), - } - .into() - } - - fn container(title: &str) -> Column<'_, StepMessage> { - column![text(title).size(50)].spacing(20) - } - - fn welcome() -> Column<'a, StepMessage> { + fn welcome(&self) -> Column { Self::container("Welcome!") .push( "This is a simple tour meant to showcase a bunch of widgets \ @@ -389,7 +224,7 @@ impl<'a> Step { ) } - fn slider(value: u8) -> Column<'a, StepMessage> { + fn slider(&self) -> Column { Self::container("Slider") .push( "A slider allows you to smoothly select a value from a range \ @@ -399,47 +234,48 @@ impl<'a> Step { "The following slider lets you choose an integer from \ 0 to 100:", ) - .push(slider(0..=100, value, StepMessage::SliderChanged)) + .push(slider(0..=100, self.slider, Message::SliderChanged)) .push( - text(value.to_string()) + text(self.slider.to_string()) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) } - fn rows_and_columns( - layout: Layout, - spacing: u16, - ) -> Column<'a, StepMessage> { - let row_radio = - radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); + fn rows_and_columns(&self) -> Column { + let row_radio = radio( + "Row", + Layout::Row, + Some(self.layout), + Message::LayoutChanged, + ); let column_radio = radio( "Column", Layout::Column, - Some(layout), - StepMessage::LayoutChanged, + Some(self.layout), + Message::LayoutChanged, ); - let layout_section: Element<_> = match layout { + let layout_section: Element<_> = match self.layout { Layout::Row => { - row![row_radio, column_radio].spacing(spacing).into() - } - Layout::Column => { - column![row_radio, column_radio].spacing(spacing).into() + row![row_radio, column_radio].spacing(self.spacing).into() } + Layout::Column => column![row_radio, column_radio] + .spacing(self.spacing) + .into(), }; let spacing_section = column![ - slider(0..=80, spacing, StepMessage::SpacingChanged), - text!("{spacing} px") + slider(0..=80, self.spacing, Message::SpacingChanged), + text!("{} px", self.spacing) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ] .spacing(10); Self::container("Rows and columns") - .spacing(spacing) + .spacing(self.spacing) .push( "Iced uses a layout model based on flexbox to position UI \ elements.", @@ -453,11 +289,14 @@ impl<'a> Step { .push(spacing_section) } - fn text(size: u16, color: Color) -> Column<'a, StepMessage> { + fn text(&self) -> Column { + let size = self.text_size; + let color = self.text_color; + let size_section = column![ "You can change its size:", text!("This text is {size} pixels").size(size), - slider(10..=70, size, StepMessage::TextSizeChanged), + slider(10..=70, size, Message::TextSizeChanged), ] .padding(20) .spacing(20); @@ -486,7 +325,7 @@ impl<'a> Step { .push(color_section) } - fn radio(selection: Option) -> Column<'a, StepMessage> { + fn radio(&self) -> Column { let question = column![ text("Iced is written in...").size(24), column( @@ -497,8 +336,8 @@ impl<'a> Step { radio( language, language, - selection, - StepMessage::LanguageSelected, + self.language, + Message::LanguageSelected, ) }) .map(Element::from) @@ -521,27 +360,27 @@ impl<'a> Step { ) } - fn toggler(can_continue: bool) -> Column<'a, StepMessage> { + fn toggler(&self) -> Column { Self::container("Toggler") .push("A toggler is mostly used to enable or disable something.") .push( Container::new(toggler( "Toggle me to continue...".to_owned(), - can_continue, - StepMessage::TogglerChanged, + self.toggler, + Message::TogglerChanged, )) .padding([0, 40]), ) } - fn image( - width: u16, - filter_method: image::FilterMethod, - ) -> Column<'a, StepMessage> { + fn image(&self) -> Column { + let width = self.image_width; + let filter_method = self.image_filter_method; + Self::container("Image") .push("An image that tries to keep its aspect ratio.") .push(ferris(width, filter_method)) - .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) + .push(slider(100..=500, width, Message::ImageWidthChanged)) .push( text!("Width: {width} px") .width(Length::Fill) @@ -552,12 +391,12 @@ impl<'a> Step { "Use nearest interpolation", filter_method == image::FilterMethod::Nearest, ) - .on_toggle(StepMessage::ImageUseNearestToggled), + .on_toggle(Message::ImageUseNearestToggled), ) .align_items(Alignment::Center) } - fn scrollable() -> Column<'a, StepMessage> { + fn scrollable(&self) -> Column { Self::container("Scrollable") .push( "Iced supports scrollable content. Try it out! Find the \ @@ -584,13 +423,13 @@ impl<'a> Step { ) } - fn text_input( - value: &str, - is_secure: bool, - is_showing_icon: bool, - ) -> Column<'_, StepMessage> { + fn text_input(&self) -> Column { + let value = &self.input_value; + let is_secure = self.input_is_secure; + let is_showing_icon = self.input_is_showing_icon; + let mut text_input = text_input("Type something to continue...", value) - .on_input(StepMessage::InputChanged) + .on_input(Message::InputChanged) .padding(10) .size(30); @@ -609,11 +448,11 @@ impl<'a> Step { .push(text_input.secure(is_secure)) .push( checkbox("Enable password mode", is_secure) - .on_toggle(StepMessage::ToggleSecureInput), + .on_toggle(Message::ToggleSecureInput), ) .push( checkbox("Show icon", is_showing_icon) - .on_toggle(StepMessage::ToggleTextInputIcon), + .on_toggle(Message::ToggleTextInputIcon), ) .push( "A text input produces a message every time it changes. It is \ @@ -630,7 +469,7 @@ impl<'a> Step { ) } - fn debugger(debug: bool) -> Column<'a, StepMessage> { + fn debugger(&self) -> Column { Self::container("Debugger") .push( "You can ask Iced to visually explain the layouting of the \ @@ -641,23 +480,85 @@ impl<'a> Step { see element boundaries.", ) .push( - checkbox("Explain layout", debug) - .on_toggle(StepMessage::DebugToggled), + checkbox("Explain layout", self.debug) + .on_toggle(Message::DebugToggled), ) .push("Feel free to go back and take a look.") } - fn end() -> Column<'a, StepMessage> { + fn end(&self) -> Column { 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!") } + + fn container(title: &str) -> Column<'_, Message> { + column![text(title).size(50)].spacing(20) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Step { + Welcome, + Slider, + RowsAndColumns, + Text, + Radio, + Toggler, + Image, + Scrollable, + TextInput, + Debugger, + End, +} + +impl Step { + const ALL: &'static [Self] = &[ + Self::Welcome, + Self::Slider, + Self::RowsAndColumns, + Self::Text, + Self::Radio, + Self::Toggler, + Self::Image, + Self::Scrollable, + Self::TextInput, + Self::Debugger, + Self::End, + ]; + + pub fn next(self) -> Option { + Self::ALL + .get( + Self::ALL + .iter() + .copied() + .position(|step| step == self) + .expect("Step must exist") + + 1, + ) + .copied() + } + + pub fn previous(self) -> Option { + let position = Self::ALL + .iter() + .copied() + .position(|step| step == self) + .expect("Step must exist"); + + if position > 0 { + Some(Self::ALL[position - 1]) + } else { + None + } + } } fn ferris<'a>( width: u16, filter_method: image::FilterMethod, -) -> Container<'a, StepMessage> { +) -> Container<'a, Message> { container( // This should go away once we unify resource loading on native // platforms @@ -679,9 +580,9 @@ fn padded_button(label: &str) -> Button<'_, Message> { fn color_slider<'a>( component: f32, update: impl Fn(f32) -> Color + 'a, -) -> Slider<'a, f64, StepMessage> { +) -> Slider<'a, f64, Message> { slider(0.0..=1.0, f64::from(component), move |c| { - StepMessage::TextColorChanged(update(c as f32)) + Message::TextColorChanged(update(c as f32)) }) .step(0.01) } @@ -727,3 +628,24 @@ pub enum Layout { Row, Column, } + +impl Default for Tour { + fn default() -> Self { + Self { + step: Step::Welcome, + slider: 50, + layout: Layout::Row, + spacing: 20, + text_size: 30, + text_color: Color::BLACK, + language: None, + toggler: false, + image_width: 300, + image_filter_method: image::FilterMethod::Linear, + input_value: String::new(), + input_is_secure: false, + input_is_showing_icon: false, + debug: false, + } + } +} From e9141e7abff3ea59e8bc5d8e2e386d564180e0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 16 Jun 2024 23:18:45 +0200 Subject: [PATCH 055/657] Rename `Step` to `Screen` in `tour` example --- examples/tour/src/main.rs | 102 +++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 6b10ec75..78086ce9 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -22,7 +22,7 @@ pub fn main() -> iced::Result { } pub struct Tour { - step: Step, + screen: Screen, slider: u8, layout: Layout, spacing: u16, @@ -59,18 +59,18 @@ pub enum Message { impl Tour { fn title(&self) -> String { - let screen = match self.step { - Step::Welcome => "Welcome", - Step::Radio => "Radio button", - Step::Toggler => "Toggler", - Step::Slider => "Slider", - Step::Text => "Text", - Step::Image => "Image", - Step::RowsAndColumns => "Rows and columns", - Step::Scrollable => "Scrollable", - Step::TextInput => "Text input", - Step::Debugger => "Debugger", - Step::End => "End", + let screen = match self.screen { + Screen::Welcome => "Welcome", + Screen::Radio => "Radio button", + Screen::Toggler => "Toggler", + Screen::Slider => "Slider", + Screen::Text => "Text", + Screen::Image => "Image", + Screen::RowsAndColumns => "Rows and columns", + Screen::Scrollable => "Scrollable", + Screen::TextInput => "Text input", + Screen::Debugger => "Debugger", + Screen::End => "End", }; format!("{} - Iced", screen) @@ -79,13 +79,13 @@ impl Tour { fn update(&mut self, event: Message) { match event { Message::BackPressed => { - if let Some(step) = self.step.previous() { - self.step = step; + if let Some(screen) = self.screen.previous() { + self.screen = screen; } } Message::NextPressed => { - if let Some(step) = self.step.next() { - self.step = step; + if let Some(screen) = self.screen.next() { + self.screen = screen; } } Message::SliderChanged(value) => { @@ -137,7 +137,7 @@ impl Tour { fn view(&self) -> Element { let controls = row![] - .push_maybe(self.step.previous().is_some().then(|| { + .push_maybe(self.screen.previous().is_some().then(|| { padded_button("Back") .on_press(Message::BackPressed) .style(button::secondary) @@ -147,18 +147,18 @@ impl Tour { padded_button("Next").on_press(Message::NextPressed) })); - let screen = match self.step { - Step::Welcome => self.welcome(), - Step::Radio => self.radio(), - Step::Toggler => self.toggler(), - Step::Slider => self.slider(), - Step::Text => self.text(), - Step::Image => self.image(), - Step::RowsAndColumns => self.rows_and_columns(), - Step::Scrollable => self.scrollable(), - Step::TextInput => self.text_input(), - Step::Debugger => self.debugger(), - Step::End => self.end(), + let screen = match self.screen { + Screen::Welcome => self.welcome(), + Screen::Radio => self.radio(), + Screen::Toggler => self.toggler(), + Screen::Slider => self.slider(), + Screen::Text => self.text(), + Screen::Image => self.image(), + Screen::RowsAndColumns => self.rows_and_columns(), + Screen::Scrollable => self.scrollable(), + Screen::TextInput => self.text_input(), + Screen::Debugger => self.debugger(), + Screen::End => self.end(), }; let content: Element<_> = column![screen, controls,] @@ -180,18 +180,18 @@ impl Tour { } fn can_continue(&self) -> bool { - match self.step { - Step::Welcome => true, - Step::Radio => self.language == Some(Language::Rust), - Step::Toggler => self.toggler, - Step::Slider => true, - Step::Text => true, - Step::Image => true, - Step::RowsAndColumns => true, - Step::Scrollable => true, - Step::TextInput => !self.input_value.is_empty(), - Step::Debugger => true, - Step::End => false, + match self.screen { + Screen::Welcome => true, + Screen::Radio => self.language == Some(Language::Rust), + Screen::Toggler => self.toggler, + Screen::Slider => true, + Screen::Text => true, + Screen::Image => true, + Screen::RowsAndColumns => true, + Screen::Scrollable => true, + Screen::TextInput => !self.input_value.is_empty(), + Screen::Debugger => true, + Screen::End => false, } } @@ -498,7 +498,7 @@ impl Tour { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Step { +enum Screen { Welcome, Slider, RowsAndColumns, @@ -512,7 +512,7 @@ enum Step { End, } -impl Step { +impl Screen { const ALL: &'static [Self] = &[ Self::Welcome, Self::Slider, @@ -527,25 +527,25 @@ impl Step { Self::End, ]; - pub fn next(self) -> Option { + pub fn next(self) -> Option { Self::ALL .get( Self::ALL .iter() .copied() - .position(|step| step == self) - .expect("Step must exist") + .position(|screen| screen == self) + .expect("Screen must exist") + 1, ) .copied() } - pub fn previous(self) -> Option { + pub fn previous(self) -> Option { let position = Self::ALL .iter() .copied() - .position(|step| step == self) - .expect("Step must exist"); + .position(|screen| screen == self) + .expect("Screen must exist"); if position > 0 { Some(Self::ALL[position - 1]) @@ -632,7 +632,7 @@ pub enum Layout { impl Default for Tour { fn default() -> Self { Self { - step: Step::Welcome, + screen: Screen::Welcome, slider: 50, layout: Layout::Row, spacing: 20, From 6c1027af8d54ad21e282337b53097eb196d62c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 17 Jun 2024 03:37:00 +0200 Subject: [PATCH 056/657] Fix `text_editor` always capturing scroll events --- graphics/src/text/editor.rs | 12 ++++-------- widget/src/text_editor.rs | 6 ++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4b8f0f2a..c488a51c 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -456,14 +456,10 @@ impl editor::Editor for Editor { } } Action::Scroll { lines } => { - let (_, height) = editor.buffer().size(); - - if height < i32::MAX as f32 { - editor.action( - font_system.raw(), - cosmic_text::Action::Scroll { lines }, - ); - } + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 41b058af..fc2ade43 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -466,6 +466,12 @@ where shell.publish(on_edit(action)); } Update::Scroll(lines) => { + let bounds = self.content.0.borrow().editor.bounds(); + + if bounds.height >= i32::MAX as f32 { + return event::Status::Ignored; + } + let lines = lines + state.partial_scroll; state.partial_scroll = lines.fract(); From 19db068bbbebcda1756720525da247f35bd3a5e0 Mon Sep 17 00:00:00 2001 From: SolidStateDj Date: Tue, 18 Jun 2024 13:02:15 -0400 Subject: [PATCH 057/657] Implement `std::fmt::Display` for `iced::Radians` (#2446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement `std::fmt::Display` for Radians * Add ` rad` to the end of all displayed strings. Co-authored-by: Héctor Ramón --------- Co-authored-by: Héctor Ramón --- core/src/angle.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/angle.rs b/core/src/angle.rs index 9c8a9b24..0882ae80 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -1,6 +1,7 @@ use crate::{Point, Rectangle, Vector}; use std::f32::consts::{FRAC_PI_2, PI}; +use std::fmt::Display; use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign}; /// Degrees @@ -237,3 +238,9 @@ impl PartialOrd for Radians { self.0.partial_cmp(other) } } + +impl Display for Radians { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} rad", self.0) + } +} From 341c9a3c12aa9d327ef1d8f168ea0adb9b5ad10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 19 Jun 2024 01:53:40 +0200 Subject: [PATCH 058/657] Introduce `daemon` API and unify shell runtimes --- Cargo.toml | 2 +- examples/arc/src/main.rs | 2 +- examples/bezier_tool/src/main.rs | 2 +- examples/checkbox/src/main.rs | 2 +- examples/clock/src/main.rs | 2 +- examples/color_palette/src/main.rs | 2 +- examples/custom_shader/src/main.rs | 10 +- examples/download_progress/src/main.rs | 10 +- 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 | 16 +- examples/gradient/src/main.rs | 10 +- examples/layout/src/main.rs | 2 +- examples/loading_spinners/src/main.rs | 2 +- examples/modal/src/main.rs | 2 +- examples/multi_window/src/main.rs | 39 +- examples/multitouch/src/main.rs | 2 +- examples/pane_grid/src/main.rs | 2 +- examples/pokedex/src/main.rs | 2 +- examples/qr_code/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- examples/scrollable/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 2 +- examples/solar_system/src/main.rs | 2 +- examples/stopwatch/src/main.rs | 2 +- examples/styling/src/main.rs | 2 +- examples/system_information/src/main.rs | 8 +- examples/the_matrix/src/main.rs | 2 +- examples/toast/src/main.rs | 2 +- examples/todos/src/main.rs | 2 +- examples/tour/src/main.rs | 2 +- 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 +- runtime/src/lib.rs | 16 + src/advanced.rs | 1 - src/application.rs | 616 ++++++---- src/daemon.rs | 298 +++++ src/lib.rs | 33 +- src/multi_window.rs | 254 ---- src/program.rs | 830 +++++-------- src/settings.rs | 49 +- winit/Cargo.toml | 2 +- winit/src/application.rs | 1082 ----------------- winit/src/application/state.rs | 221 ---- winit/src/lib.rs | 15 +- winit/src/{multi_window.rs => program.rs} | 340 +++--- winit/src/{multi_window => program}/state.rs | 32 +- .../window_manager.rs | 60 +- winit/src/proxy.rs | 13 +- winit/src/settings.rs | 12 +- 54 files changed, 1352 insertions(+), 2677 deletions(-) create mode 100644 src/daemon.rs delete mode 100644 src/multi_window.rs delete mode 100644 winit/src/application.rs delete mode 100644 winit/src/application/state.rs rename winit/src/{multi_window.rs => program.rs} (85%) rename winit/src/{multi_window => program}/state.rs (90%) rename winit/src/{multi_window => program}/window_manager.rs (73%) diff --git a/Cargo.toml b/Cargo.toml index 44b3d307..b85900cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_widget.workspace = true -iced_winit.features = ["application"] +iced_winit.features = ["program"] iced_winit.workspace = true iced_highlighter.workspace = true diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 4576404f..b1e8402a 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -7,7 +7,7 @@ use iced::widget::canvas::{ use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme}; pub fn main() -> iced::Result { - iced::program("Arc - Iced", Arc::update, Arc::view) + iced::application("Arc - Iced", Arc::update, Arc::view) .subscription(Arc::subscription) .theme(|_| Theme::Dark) .antialiasing(true) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 29df3eeb..eaf84b97 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{button, container, horizontal_space, hover}; use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { - iced::program("Bezier Tool - Iced", Example::update, Example::view) + iced::application("Bezier Tool - Iced", Example::update, Example::view) .theme(|_| Theme::CatppuccinMocha) .antialiasing(true) .run() diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index bec4a954..f06557f8 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -4,7 +4,7 @@ use iced::{Element, Font}; const ICON_FONT: Font = Font::with_name("icons"); pub fn main() -> iced::Result { - iced::program("Checkbox - Iced", Example::update, Example::view) + iced::application("Checkbox - Iced", Example::update, Example::view) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .run() } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 7c4685c4..4584a0c7 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -11,7 +11,7 @@ use iced::{ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Clock - Iced", Clock::update, Clock::view) + iced::application("Clock - Iced", Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) .antialiasing(true) diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index d9325edb..e4b19731 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -11,7 +11,7 @@ use std::marker::PhantomData; use std::ops::RangeInclusive; pub fn main() -> iced::Result { - iced::program( + iced::application( "Color Palette - Iced", ColorPalette::update, ColorPalette::view, diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 463b2df9..b04a8183 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -9,9 +9,13 @@ use iced::window; use iced::{Alignment, Color, Element, Length, Subscription}; fn main() -> iced::Result { - iced::program("Custom Shader - Iced", IcedCubes::update, IcedCubes::view) - .subscription(IcedCubes::subscription) - .run() + iced::application( + "Custom Shader - Iced", + IcedCubes::update, + IcedCubes::view, + ) + .subscription(IcedCubes::subscription) + .run() } struct IcedCubes { diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 7974d5a0..d91e5eab 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -4,9 +4,13 @@ use iced::widget::{button, center, column, progress_bar, text, Column}; use iced::{Alignment, Element, Subscription}; pub fn main() -> iced::Result { - iced::program("Download Progress - Iced", Example::update, Example::view) - .subscription(Example::subscription) - .run() + iced::application( + "Download Progress - Iced", + Example::update, + Example::view, + ) + .subscription(Example::subscription) + .run() } #[derive(Debug)] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index ec65e2fa..bed9d94a 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; pub fn main() -> iced::Result { - iced::program("Editor - Iced", Editor::update, Editor::view) + iced::application("Editor - Iced", Editor::update, Editor::view) .load(Editor::load) .subscription(Editor::subscription) .theme(Editor::theme) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 504ed5d8..4f0f07b0 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -5,7 +5,7 @@ use iced::window; use iced::{Alignment, Element, Length, Subscription, Task}; pub fn main() -> iced::Result { - iced::program("Events - Iced", Events::update, Events::view) + iced::application("Events - Iced", Events::update, Events::view) .subscription(Events::subscription) .exit_on_close_request(false) .run() diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 8ba180a5..b998016e 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -3,7 +3,7 @@ use iced::window; use iced::{Alignment, Element, Task}; pub fn main() -> iced::Result { - iced::program("Exit - Iced", Exit::update, Exit::view).run() + iced::application("Exit - Iced", Exit::update, Exit::view).run() } #[derive(Default)] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 0400c376..88006898 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -9,7 +9,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::program("Ferris - Iced", Image::update, Image::view) + iced::application("Ferris - Iced", Image::update, Image::view) .subscription(Image::subscription) .theme(|_| Theme::TokyoNight) .run() diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 7e6d461d..421f862a 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -15,12 +15,16 @@ use std::time::Duration; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Game of Life - Iced", GameOfLife::update, GameOfLife::view) - .subscription(GameOfLife::subscription) - .theme(|_| Theme::Dark) - .antialiasing(true) - .centered() - .run() + iced::application( + "Game of Life - Iced", + GameOfLife::update, + GameOfLife::view, + ) + .subscription(GameOfLife::subscription) + .theme(|_| Theme::Dark) + .antialiasing(true) + .centered() + .run() } struct GameOfLife { diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 2b906c32..e5b19443 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -1,5 +1,5 @@ +use iced::application; use iced::gradient; -use iced::program; use iced::widget::{ checkbox, column, container, horizontal_space, row, slider, text, }; @@ -8,7 +8,7 @@ use iced::{Alignment, Color, Element, Length, Radians, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Gradient - Iced", Gradient::update, Gradient::view) + iced::application("Gradient - Iced", Gradient::update, Gradient::view) .style(Gradient::style) .transparent(true) .run() @@ -95,11 +95,11 @@ impl Gradient { .into() } - fn style(&self, theme: &Theme) -> program::Appearance { - use program::DefaultStyle; + fn style(&self, theme: &Theme) -> application::Appearance { + use application::DefaultStyle; if self.transparent { - program::Appearance { + application::Appearance { background_color: Color::TRANSPARENT, text_color: theme.palette().text, } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index c40ac820..2e774415 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -10,7 +10,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::program(Layout::title, Layout::update, Layout::view) + iced::application(Layout::title, Layout::update, Layout::view) .subscription(Layout::subscription) .theme(Layout::theme) .run() diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index a63c51d4..503f2d7a 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -11,7 +11,7 @@ use circular::Circular; use linear::Linear; pub fn main() -> iced::Result { - iced::program( + iced::application( "Loading Spinners - Iced", LoadingSpinners::update, LoadingSpinners::view, diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index d185cf3b..413485e7 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -10,7 +10,7 @@ use iced::{Alignment, Color, Element, Length, Subscription, Task}; use std::fmt; pub fn main() -> iced::Result { - iced::program("Modal - Iced", App::update, App::view) + iced::application("Modal - Iced", App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index b82ad1f3..dfb816cf 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,18 +1,21 @@ -use iced::executor; -use iced::multi_window::{self, Application}; use iced::widget::{ button, center, column, container, horizontal_space, scrollable, text, text_input, }; use iced::window; -use iced::{ - Alignment, Element, Length, Settings, Subscription, Task, Theme, Vector, -}; +use iced::{Alignment, Element, Length, Subscription, Task, Theme, Vector}; use std::collections::BTreeMap; fn main() -> iced::Result { - Example::run(Settings::default()) + iced::daemon(Example::title, Example::update, Example::view) + .load(|| { + window::open(window::Settings::default()).map(Message::WindowOpened) + }) + .subscription(Example::subscription) + .theme(Example::theme) + .scale_factor(Example::scale_factor) + .run() } #[derive(Default)] @@ -39,21 +42,7 @@ enum Message { TitleChanged(window::Id, String), } -impl multi_window::Application for Example { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Task) { - ( - Example { - windows: BTreeMap::from([(window::Id::MAIN, Window::new(1))]), - }, - Task::none(), - ) - } - +impl Example { fn title(&self, window: window::Id) -> String { self.windows .get(&window) @@ -97,7 +86,11 @@ impl multi_window::Application for Example { Message::WindowClosed(id) => { self.windows.remove(&id); - Task::none() + if self.windows.is_empty() { + iced::exit() + } else { + Task::none() + } } Message::ScaleInputChanged(id, scale) => { if let Some(window) = self.windows.get_mut(&id) { @@ -149,7 +142,7 @@ impl multi_window::Application for Example { .unwrap_or(1.0) } - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { window::close_events().map(Message::WindowClosed) } } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 2453c7f5..69717310 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -13,7 +13,7 @@ use std::collections::HashMap; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Multitouch - Iced", Multitouch::update, Multitouch::view) + iced::application("Multitouch - Iced", Multitouch::update, Multitouch::view) .antialiasing(true) .centered() .run() diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 6b5bd332..db9f7a05 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -7,7 +7,7 @@ use iced::widget::{ use iced::{Color, Element, Length, Size, Subscription}; pub fn main() -> iced::Result { - iced::program("Pane Grid - Iced", Example::update, Example::view) + iced::application("Pane Grid - Iced", Example::update, Example::view) .subscription(Example::subscription) .run() } diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index e62ed70b..b22ffe7f 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{self, center, column, image, row, text}; use iced::{Alignment, Element, Length, Task}; pub fn main() -> iced::Result { - iced::program(Pokedex::title, Pokedex::update, Pokedex::view) + iced::application(Pokedex::title, Pokedex::update, Pokedex::view) .load(Pokedex::search) .run() } diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index c6a90458..b30ecf15 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; use iced::{Alignment, Element, Theme}; pub fn main() -> iced::Result { - iced::program( + iced::application( "QR Code Generator - Iced", QRGenerator::update, QRGenerator::view, diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 78d3e9ff..1ea53e8f 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -13,7 +13,7 @@ use ::image::ColorType; fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Screenshot - Iced", Example::update, Example::view) + iced::application("Screenshot - Iced", Example::update, Example::view) .subscription(Example::subscription) .run() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index a0dcf82c..f2a853e1 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -10,7 +10,7 @@ use once_cell::sync::Lazy; static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); pub fn main() -> iced::Result { - iced::program( + iced::application( "Scrollable - Iced", ScrollableDemo::update, ScrollableDemo::view, diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 7dd7be5e..4c751937 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -8,7 +8,7 @@ use rand::Rng; use std::fmt::Debug; fn main() -> iced::Result { - iced::program( + iced::application( "Sierpinski Triangle - Iced", SierpinskiEmulator::update, SierpinskiEmulator::view, diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index deb211d8..2a67e23e 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -22,7 +22,7 @@ use std::time::Instant; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program( + iced::application( "Solar System - Iced", SolarSystem::update, SolarSystem::view, diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index a8149753..bd56785a 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -7,7 +7,7 @@ use iced::{Alignment, Element, Subscription, Theme}; use std::time::{Duration, Instant}; pub fn main() -> iced::Result { - iced::program("Stopwatch - Iced", Stopwatch::update, Stopwatch::view) + iced::application("Stopwatch - Iced", Stopwatch::update, Stopwatch::view) .subscription(Stopwatch::subscription) .theme(Stopwatch::theme) .run() diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 57e8f47e..3124493b 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ use iced::{Alignment, Element, Length, Theme}; pub fn main() -> iced::Result { - iced::program("Styling - Iced", Styling::update, Styling::view) + iced::application("Styling - Iced", Styling::update, Styling::view) .theme(Styling::theme) .run() } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index e2808edd..363df590 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -2,8 +2,12 @@ use iced::widget::{button, center, column, text}; use iced::{system, Element, Task}; pub fn main() -> iced::Result { - iced::program("System Information - Iced", Example::update, Example::view) - .run() + iced::application( + "System Information - Iced", + Example::update, + Example::view, + ) + .run() } #[derive(Default)] diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index f3a67ac8..2ae1cc3a 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -11,7 +11,7 @@ use std::cell::RefCell; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) + iced::application("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) .antialiasing(true) .run() diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index aee2479e..232133b1 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -9,7 +9,7 @@ use iced::{Alignment, Element, Length, Subscription, Task}; use toast::{Status, Toast}; pub fn main() -> iced::Result { - iced::program("Toast - Iced", App::update, App::view) + iced::application("Toast - Iced", App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index c21e1a96..a834c946 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -17,7 +17,7 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::program(Todos::title, Todos::update, Todos::view) + iced::application(Todos::title, Todos::update, Todos::view) .load(Todos::load) .subscription(Todos::subscription) .font(include_bytes!("../fonts/icons.ttf").as_slice()) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 78086ce9..94ba78ee 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -16,7 +16,7 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::program(Tour::title, Tour::update, Tour::view) + iced::application(Tour::title, Tour::update, Tour::view) .centered() .run() } diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 3ab19252..50a055f3 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{center, text}; use iced::{Element, Subscription}; pub fn main() -> iced::Result { - iced::program("URL Handler - Iced", App::update, App::view) + iced::application("URL Handler - Iced", App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 1ed7a2b1..6dd3273a 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ use iced::{Element, Length, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { - iced::program( + iced::application( "Vectorial Text - Iced", VectorialText::update, VectorialText::view, diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index b43c0cca..e46d1ff0 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -10,7 +10,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::program("Visible Bounds - Iced", Example::update, Example::view) + iced::application("Visible Bounds - Iced", Example::update, Example::view) .subscription(Example::subscription) .theme(|_| Theme::Dark) .run() diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 8c0fa1d0..8422ce16 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -8,7 +8,7 @@ use iced::{color, Element, Length, Subscription, Task}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { - iced::program("WebSocket - Iced", WebSocket::update, WebSocket::view) + iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view) .load(WebSocket::load) .subscription(WebSocket::subscription) .run() diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5fde3039..b4a5e819 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -70,6 +70,12 @@ pub enum Action { /// Run a system action. System(system::Action), + + /// Exits the runtime. + /// + /// This will normally close any application windows and + /// terminate the runtime loop. + Exit, } impl Action { @@ -88,6 +94,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::Exit => Err(Action::Exit), } } } @@ -110,6 +117,15 @@ where } Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Exit => write!(f, "Action::Exit"), } } } + +/// Creates a [`Task`] that exits the iced runtime. +/// +/// This will normally close any application windows and +/// terminate the runtime loop. +pub fn exit() -> Task { + Task::effect(Action::Exit) +} diff --git a/src/advanced.rs b/src/advanced.rs index 5826ba0f..8d06e805 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,5 +1,4 @@ //! Leverage advanced concepts like custom widgets. -pub use crate::application::Application; pub use crate::core::clipboard::{self, Clipboard}; pub use crate::core::image; pub use crate::core::layout::{self, Layout}; diff --git a/src/application.rs b/src/application.rs index 4cd4a87d..edca6e79 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,278 +1,426 @@ -//! Build interactive cross-platform applications. -use crate::core::text; -use crate::graphics::compositor; -use crate::shell::application; -use crate::{Element, Executor, Settings, Subscription, Task}; +//! Create and run iced applications step by step. +//! +//! # Example +//! ```no_run +//! use iced::widget::{button, column, text, Column}; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::application("A counter", update, view) +//! .theme(|_| Theme::Dark) +//! .centered() +//! .run() +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! +//! fn update(value: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *value += 1, +//! } +//! } +//! +//! fn view(value: &u64) -> Column { +//! column![ +//! text(value), +//! button("+").on_press(Message::Increment), +//! ] +//! } +//! ``` +use crate::program::{self, Program}; +use crate::window; +use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; -pub use application::{Appearance, DefaultStyle}; +use std::borrow::Cow; -/// An interactive cross-platform application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`](#method.run). -/// -/// - On native platforms, it will run in its own window. -/// - On the web, it will take control of the `` and the `<body>` of the -/// document. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Task`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -/// -/// # Examples -/// [The repository has a bunch of examples] that use the [`Application`] trait: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`download_progress`], a basic application that asynchronously downloads -/// a dummy file of 100 MB and tracks the download progress. -/// - [`events`], a log of native events displayed using a conditional -/// [`Subscription`]. -/// - [`game_of_life`], an interactive version of the [Game of Life], invented -/// by [John Horton Conway]. -/// - [`pokedex`], an application that displays a random Pokédex entry (sprite -/// included!) by using the [PokéAPI]. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how -/// to listen to time. -/// - [`todos`], a todos tracker inspired by [TodoMVC]. -/// -/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.12/examples -/// [`clock`]: https://github.com/iced-rs/iced/tree/0.12/examples/clock -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress -/// [`events`]: https://github.com/iced-rs/iced/tree/0.12/examples/events -/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.12/examples/game_of_life -/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.12/examples/pokedex -/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.12/examples/solar_system -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.12/examples/stopwatch -/// [`todos`]: https://github.com/iced-rs/iced/tree/0.12/examples/todos -/// [`Sandbox`]: crate::Sandbox -/// [`Canvas`]: crate::widget::Canvas -/// [PokéAPI]: https://pokeapi.co/ -/// [TodoMVC]: http://todomvc.com/ -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Application`] that -/// says "Hello, world!": +pub use crate::shell::program::{Appearance, DefaultStyle}; + +/// Creates an iced [`Application`] given its title, update, and view logic. /// +/// # Example /// ```no_run -/// use iced::advanced::Application; -/// use iced::executor; -/// use iced::{Task, Element, Settings, Theme, Renderer}; +/// use iced::widget::{button, column, text, Column}; /// /// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) +/// iced::application("A counter", update, view).run() /// } /// -/// struct Hello; +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } /// -/// impl Application for Hello { -/// type Executor = executor::Default; -/// type Flags = (); -/// type Message = (); -/// type Theme = Theme; -/// type Renderer = Renderer; -/// -/// fn new(_flags: ()) -> (Hello, Task<Self::Message>) { -/// (Hello, Task::none()) +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, /// } +/// } /// -/// fn title(&self) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) -> Task<Self::Message> { -/// Task::none() -/// } -/// -/// fn view(&self) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } +/// fn view(value: &u64) -> Column<Message> { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] /// } /// ``` -pub trait Application: Sized +pub fn application<State, Message, Theme, Renderer>( + title: impl Title<State>, + update: impl Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Application<impl Program<State = State, Message = Message, Theme = Theme>> where - Self::Theme: DefaultStyle, + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, { - /// The [`Executor`] that will run commands and subscriptions. - /// - /// The [default executor] can be a good starting point! - /// - /// [`Executor`]: Self::Executor - /// [default executor]: crate::executor::Default - type Executor: Executor; + use std::marker::PhantomData; - /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme of your [`Application`]. - type Theme: Default; - - /// The renderer of your [`Application`]. - type Renderer: text::Renderer + compositor::Default; - - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); - - /// Returns the current title of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self) -> String; - - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Task`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Task<Self::Message>; - - /// Returns the widgets to display in the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; - - /// Returns the current [`Theme`] of the [`Application`]. - /// - /// [`Theme`]: Self::Theme - fn theme(&self) -> Self::Theme { - Self::Theme::default() + struct Instance<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, } - /// Returns the current [`Appearance`] of the [`Application`]. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() + impl<State, Message, Theme, Renderer, Update, View> Program + for Instance<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, + Update: self::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; + + fn load(&self) -> Task<Self::Message> { + Task::none() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + _window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state).into() + } } - /// Returns the event [`Subscription`] for the current state of the - /// application. - /// - /// A [`Subscription`] will be kept alive as long as you keep returning it, - /// and the __messages__ produced will be handled by - /// [`update`](#tymethod.update). - /// - /// By default, this method returns an empty [`Subscription`]. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() + Application { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + window: window::Settings::default(), } + .title(title) +} - /// Returns the scale factor of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { - 1.0 - } +/// The underlying definition and configuration of an iced application. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create an [`Application`] with the [`application`] helper. +#[derive(Debug)] +pub struct Application<P: Program> { + raw: P, + settings: Settings, + window: window::Settings, +} +impl<P: Program> Application<P> { /// Runs the [`Application`]. /// - /// On native platforms, this method will take control of the current thread - /// until the [`Application`] exits. + /// The state of the [`Application`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. /// - /// On the web platform, this method __will NOT return__ unless there is an - /// [`Error`] during startup. - /// - /// [`Error`]: crate::Error - fn run(settings: Settings<Self::Flags>) -> crate::Result + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result where Self: 'static, + P::State: Default, { - #[allow(clippy::needless_update)] - let renderer_settings = crate::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None + self.run_with(P::State::default) + } + + /// Runs the [`Application`] with a closure that creates the initial state. + pub fn run_with<I>(self, initialize: I) -> Result + where + Self: 'static, + I: Fn() -> P::State + Clone + 'static, + { + self.raw + .run_with(self.settings, Some(self.window), initialize) + } + + /// Sets the [`Settings`] that will be used to run the [`Application`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Application`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings }, - ..crate::graphics::Settings::default() - }; + ..self + } + } - Ok(crate::shell::application::run::< - Instance<Self>, - Self::Executor, - <Self::Renderer as compositor::Default>::Compositor, - >(settings.into(), renderer_settings)?) + /// Sets the default [`Font`] of the [`Application`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Application`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Application`]. + pub fn centered(self) -> Self { + Self { + window: window::Settings { + position: window::Position::Centered, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::exit_on_close_request`] of the [`Application`]. + pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { + Self { + window: window::Settings { + exit_on_close_request, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::size`] of the [`Application`]. + pub fn window_size(self, size: impl Into<Size>) -> Self { + Self { + window: window::Settings { + size: size.into(), + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::transparent`] of the [`Application`]. + pub fn transparent(self, transparent: bool) -> Self { + Self { + window: window::Settings { + transparent, + ..self.window + }, + ..self + } + } + + /// Sets the [`Title`] of the [`Application`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_title(self.raw, move |state, _window| { + title.title(state) + }), + settings: self.settings, + window: self.window, + } + } + + /// Runs the [`Task`] produced by the closure at startup. + pub fn load( + self, + f: impl Fn() -> Task<P::Message>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_load(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the subscription logic of the [`Application`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_subscription(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the theme logic of the [`Application`]. + pub fn theme( + self, + f: impl Fn(&P::State) -> P::Theme, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_theme(self.raw, move |state, _window| f(state)), + settings: self.settings, + window: self.window, + } + } + + /// Sets the style logic of the [`Application`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_style(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the scale factor of the [`Application`]. + pub fn scale_factor( + self, + f: impl Fn(&P::State) -> f64, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_scale_factor(self.raw, move |state, _window| { + f(state) + }), + settings: self.settings, + window: self.window, + } } } -struct Instance<A>(A) -where - A: Application, - A::Theme: DefaultStyle; +/// The title logic of some [`Application`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State) -> String`. +/// +/// This trait allows the [`application`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Application`]. + fn title(&self, state: &State) -> String; +} -impl<A> crate::runtime::Program for Instance<A> +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State) -> String { + self.to_string() + } +} + +impl<T, State> Title<State> for T where - A: Application, - A::Theme: DefaultStyle, + T: Fn(&State) -> String, { - type Message = A::Message; - type Theme = A::Theme; - type Renderer = A::Renderer; - - fn update(&mut self, message: Self::Message) -> Task<Self::Message> { - self.0.update(message) - } - - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view() + fn title(&self, state: &State) -> String { + self(state) } } -impl<A> application::Application for Instance<A> +/// The update logic of some [`Application`]. +/// +/// This trait allows the [`application`] builder to take any closure that +/// returns any `Into<Task<Message>>`. +pub trait Update<State, Message> { + /// Processes the message and updates the state of the [`Application`]. + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Task<Message>>; +} + +impl<T, State, Message, C> Update<State, Message> for T where - A: Application, - A::Theme: DefaultStyle, + T: Fn(&mut State, Message) -> C, + C: Into<Task<Message>>, { - type Flags = A::Flags; - - fn new(flags: Self::Flags) -> (Self, Task<A::Message>) { - let (app, command) = A::new(flags); - - (Instance(app), command) - } - - fn title(&self) -> String { - self.0.title() - } - - fn theme(&self) -> A::Theme { - self.0.theme() - } - - fn style(&self, theme: &A::Theme) -> Appearance { - self.0.style(theme) - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() - } - - fn scale_factor(&self) -> f64 { - self.0.scale_factor() + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Task<Message>> { + self(state, message) + } +} + +/// The view logic of some [`Application`]. +/// +/// This trait allows the [`application`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Application`]. + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state) } } diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 00000000..58293949 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,298 @@ +//! Create and run daemons that run in the background. +use crate::application; +use crate::program::{self, Program}; +use crate::window; +use crate::{Element, Font, Result, Settings, Subscription, Task}; + +use std::borrow::Cow; + +pub use crate::shell::program::{Appearance, DefaultStyle}; + +/// Creates an iced [`Daemon`] given its title, update, and view logic. +/// +/// A [`Daemon`] will not open a window by default, but will run silently +/// instead until a [`Task`] from [`window::open`] is returned by its update logic. +/// +/// Furthermore, a [`Daemon`] will not stop running when all its windows are closed. +/// In order to completely terminate a [`Daemon`], its process must be interrupted or +/// its update logic must produce a [`Task`] from [`exit`]. +/// +/// [`exit`]: crate::exit +pub fn daemon<State, Message, Theme, Renderer>( + title: impl Title<State>, + update: impl application::Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>> +where + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, +{ + use std::marker::PhantomData; + + struct Instance<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, + } + + impl<State, Message, Theme, Renderer, Update, View> Program + for Instance<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, + Update: application::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; + + fn load(&self) -> Task<Self::Message> { + Task::none() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state, window).into() + } + } + + Daemon { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + } + .title(title) +} + +/// The underlying definition and configuration of an iced daemon. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create a [`Daemon`] with the [`daemon`] helper. +#[derive(Debug)] +pub struct Daemon<P: Program> { + raw: P, + settings: Settings, +} + +impl<P: Program> Daemon<P> { + /// Runs the [`Daemon`]. + /// + /// The state of the [`Daemon`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.run_with(P::State::default) + } + + /// Runs the [`Daemon`] with a closure that creates the initial state. + pub fn run_with<I>(self, initialize: I) -> Result + where + Self: 'static, + I: Fn() -> P::State + Clone + 'static, + { + self.raw.run_with(self.settings, None, initialize) + } + + /// Sets the [`Settings`] that will be used to run the [`Daemon`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Daemon`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings + }, + ..self + } + } + + /// Sets the default [`Font`] of the [`Daemon`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Daemon`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`Title`] of the [`Daemon`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_title(self.raw, move |state, window| { + title.title(state, window) + }), + settings: self.settings, + } + } + + /// Runs the [`Task`] produced by the closure at startup. + pub fn load( + self, + f: impl Fn() -> Task<P::Message>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_load(self.raw, f), + settings: self.settings, + } + } + + /// Sets the subscription logic of the [`Daemon`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_subscription(self.raw, f), + settings: self.settings, + } + } + + /// Sets the theme logic of the [`Daemon`]. + pub fn theme( + self, + f: impl Fn(&P::State, window::Id) -> P::Theme, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_theme(self.raw, f), + settings: self.settings, + } + } + + /// Sets the style logic of the [`Daemon`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_style(self.raw, f), + settings: self.settings, + } + } + + /// Sets the scale factor of the [`Daemon`]. + pub fn scale_factor( + self, + f: impl Fn(&P::State, window::Id) -> f64, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_scale_factor(self.raw, f), + settings: self.settings, + } + } +} + +/// The title logic of some [`Daemon`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State, window::Id) -> String`. +/// +/// This trait allows the [`daemon`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Daemon`]. + fn title(&self, state: &State, window: window::Id) -> String; +} + +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State, _window: window::Id) -> String { + self.to_string() + } +} + +impl<T, State> Title<State> for T +where + T: Fn(&State, window::Id) -> String, +{ + fn title(&self, state: &State, window: window::Id) -> String { + self(state, window) + } +} + +/// The view logic of some [`Daemon`]. +/// +/// This trait allows the [`daemon`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Daemon`]. + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State, window::Id) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state, window) + } +} diff --git a/src/lib.rs b/src/lib.rs index cf0bc7d7..957c7ecc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,11 +158,11 @@ //! 1. Draw the resulting user interface. //! //! # Usage -//! Use [`run`] or the [`program`] builder. +//! Use [`run`] or the [`application`] builder. //! //! [Elm]: https://elm-lang.org/ //! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! [`program`]: program() +//! [`application`]: application() #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] @@ -179,10 +179,11 @@ pub use iced_futures::futures; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; -mod application; mod error; +mod program; -pub mod program; +pub mod application; +pub mod daemon; pub mod settings; pub mod time; pub mod window; @@ -190,9 +191,6 @@ pub mod window; #[cfg(feature = "advanced")] pub mod advanced; -#[cfg(feature = "multi-window")] -pub mod multi_window; - pub use crate::core::alignment; pub use crate::core::border; pub use crate::core::color; @@ -203,7 +201,7 @@ pub use crate::core::{ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; -pub use crate::runtime::Task; +pub use crate::runtime::{exit, Task}; pub mod clipboard { //! Access the clipboard. @@ -308,11 +306,12 @@ pub mod widget { mod runtime {} } +pub use application::{application, Application}; +pub use daemon::{daemon, Daemon}; pub use error::Error; pub use event::Event; pub use executor::Executor; pub use font::Font; -pub use program::Program; pub use renderer::Renderer; pub use settings::Settings; pub use subscription::Subscription; @@ -327,13 +326,13 @@ pub type Element< Renderer = crate::Renderer, > = crate::core::Element<'a, Message, Theme, Renderer>; -/// The result of running a [`Program`]. +/// 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. /// -/// This is equivalent to chaining [`program`] with [`Program::run`]. +/// This is equivalent to chaining [`application()`] with [`Application::run`]. /// /// [`program`]: program() /// @@ -364,9 +363,10 @@ pub type Result = std::result::Result<(), Error>; /// } /// ``` pub fn run<State, Message, Theme, Renderer>( - title: impl program::Title<State> + 'static, - update: impl program::Update<State, Message> + 'static, - view: impl for<'a> program::View<'a, State, Message, Theme, Renderer> + 'static, + title: impl application::Title<State> + 'static, + update: impl application::Update<State, Message> + 'static, + view: impl for<'a> application::View<'a, State, Message, Theme, Renderer> + + 'static, ) -> Result where State: Default + 'static, @@ -374,8 +374,5 @@ where Theme: Default + program::DefaultStyle + 'static, Renderer: program::Renderer + 'static, { - program(title, update, view).run() + application(title, update, view).run() } - -#[doc(inline)] -pub use program::program; diff --git a/src/multi_window.rs b/src/multi_window.rs deleted file mode 100644 index 4900bb85..00000000 --- a/src/multi_window.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Leverage multi-window support in your application. -use crate::window; -use crate::{Element, Executor, Settings, Subscription, Task}; - -pub use crate::application::{Appearance, DefaultStyle}; - -/// An interactive cross-platform multi-window application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`](#method.run). -/// -/// - On native platforms, it will run in its own windows. -/// - On the web, it will take control of the `<title>` and the `<body>` of the -/// document and display only the contents of the `window::Id::MAIN` window. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Task`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -/// -/// # Examples -/// See the `examples/multi-window` example to see this multi-window `Application` trait in action. -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Application`] that -/// says "Hello, world!": -/// -/// ```no_run -/// use iced::{executor, window}; -/// use iced::{Task, Element, Settings, Theme}; -/// use iced::multi_window::{self, Application}; -/// -/// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) -/// } -/// -/// struct Hello; -/// -/// impl multi_window::Application for Hello { -/// type Executor = executor::Default; -/// type Flags = (); -/// type Message = (); -/// type Theme = Theme; -/// -/// fn new(_flags: ()) -> (Hello, Task<Self::Message>) { -/// (Hello, Task::none()) -/// } -/// -/// fn title(&self, _window: window::Id) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) -> Task<Self::Message> { -/// Task::none() -/// } -/// -/// fn view(&self, _window: window::Id) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } -/// } -/// ``` -/// -/// [`Sandbox`]: crate::Sandbox -pub trait Application: Sized -where - Self::Theme: DefaultStyle, -{ - /// The [`Executor`] that will run commands and subscriptions. - /// - /// The [default executor] can be a good starting point! - /// - /// [`Executor`]: Self::Executor - /// [default executor]: crate::executor::Default - type Executor: Executor; - - /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme of your [`Application`]. - type Theme: Default; - - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); - - /// Returns the current title of the `window` of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your window when necessary. - fn title(&self, window: window::Id) -> String; - - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Task`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Task<Self::Message>; - - /// Returns the widgets to display in the `window` of the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; - - /// Returns the current [`Theme`] of the `window` of the [`Application`]. - /// - /// [`Theme`]: Self::Theme - #[allow(unused_variables)] - fn theme(&self, window: window::Id) -> Self::Theme { - Self::Theme::default() - } - - /// Returns the current `Style` of the [`Theme`]. - /// - /// [`Theme`]: Self::Theme - fn style(&self, theme: &Self::Theme) -> Appearance { - Self::Theme::default_style(theme) - } - - /// Returns the event [`Subscription`] for the current state of the - /// application. - /// - /// A [`Subscription`] will be kept alive as long as you keep returning it, - /// and the __messages__ produced will be handled by - /// [`update`](#tymethod.update). - /// - /// By default, this method returns an empty [`Subscription`]. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the `window` of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - #[allow(unused_variables)] - fn scale_factor(&self, window: window::Id) -> f64 { - 1.0 - } - - /// Runs the multi-window [`Application`]. - /// - /// On native platforms, this method will take control of the current thread - /// until the [`Application`] exits. - /// - /// On the web platform, this method __will NOT return__ unless there is an - /// [`Error`] during startup. - /// - /// [`Error`]: crate::Error - fn run(settings: Settings<Self::Flags>) -> crate::Result - where - Self: 'static, - { - #[allow(clippy::needless_update)] - let renderer_settings = crate::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None - }, - ..crate::graphics::Settings::default() - }; - - Ok(crate::shell::multi_window::run::< - Instance<Self>, - Self::Executor, - crate::renderer::Compositor, - >(settings.into(), renderer_settings)?) - } -} - -struct Instance<A>(A) -where - A: Application, - A::Theme: DefaultStyle; - -impl<A> crate::runtime::multi_window::Program for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Message = A::Message; - type Theme = A::Theme; - type Renderer = crate::Renderer; - - fn update(&mut self, message: Self::Message) -> Task<Self::Message> { - self.0.update(message) - } - - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view(window) - } -} - -impl<A> crate::shell::multi_window::Application for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Flags = A::Flags; - - fn new(flags: Self::Flags) -> (Self, Task<A::Message>) { - let (app, command) = A::new(flags); - - (Instance(app), command) - } - - fn title(&self, window: window::Id) -> String { - self.0.title(window) - } - - fn theme(&self, window: window::Id) -> A::Theme { - self.0.theme(window) - } - - fn style(&self, theme: &Self::Theme) -> Appearance { - self.0.style(theme) - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() - } - - fn scale_factor(&self, window: window::Id) -> f64 { - self.0.scale_factor(window) - } -} diff --git a/src/program.rs b/src/program.rs index ea6b0e8e..3f9d2d0c 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,421 +1,17 @@ -//! Create and run iced applications step by step. -//! -//! # Example -//! ```no_run -//! use iced::widget::{button, column, text, Column}; -//! use iced::Theme; -//! -//! pub fn main() -> iced::Result { -//! iced::program("A counter", update, view) -//! .theme(|_| Theme::Dark) -//! .centered() -//! .run() -//! } -//! -//! #[derive(Debug, Clone)] -//! enum Message { -//! Increment, -//! } -//! -//! fn update(value: &mut u64, message: Message) { -//! match message { -//! Message::Increment => *value += 1, -//! } -//! } -//! -//! fn view(value: &u64) -> Column<Message> { -//! column![ -//! text(value), -//! button("+").on_press(Message::Increment), -//! ] -//! } -//! ``` -use crate::application::Application; use crate::core::text; -use crate::executor::{self, Executor}; use crate::graphics::compositor; +use crate::shell; use crate::window; -use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; +use crate::{Element, Executor, Result, Settings, Subscription, Task}; -pub use crate::application::{Appearance, DefaultStyle}; - -use std::borrow::Cow; - -/// Creates an iced [`Program`] given its title, update, and view logic. -/// -/// # Example -/// ```no_run -/// use iced::widget::{button, column, text, Column}; -/// -/// pub fn main() -> iced::Result { -/// iced::program("A counter", update, view).run() -/// } -/// -/// #[derive(Debug, Clone)] -/// enum Message { -/// Increment, -/// } -/// -/// fn update(value: &mut u64, message: Message) { -/// match message { -/// Message::Increment => *value += 1, -/// } -/// } -/// -/// fn view(value: &u64) -> Column<Message> { -/// column![ -/// text(value), -/// button("+").on_press(Message::Increment), -/// ] -/// } -/// ``` -pub fn program<State, Message, Theme, Renderer>( - title: impl Title<State>, - update: impl Update<State, Message>, - view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, -) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> -where - State: 'static, - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: self::Renderer, -{ - use std::marker::PhantomData; - - struct Application<State, Message, Theme, Renderer, Update, View> { - update: Update, - view: View, - _state: PhantomData<State>, - _message: PhantomData<Message>, - _theme: PhantomData<Theme>, - _renderer: PhantomData<Renderer>, - } - - impl<State, Message, Theme, Renderer, Update, View> Definition - for Application<State, Message, Theme, Renderer, Update, View> - where - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: self::Renderer, - Update: self::Update<State, Message>, - View: for<'a> self::View<'a, State, Message, Theme, Renderer>, - { - type State = State; - type Message = Message; - type Theme = Theme; - type Renderer = Renderer; - type Executor = executor::Default; - - fn load(&self) -> Task<Self::Message> { - Task::none() - } - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task<Self::Message> { - self.update.update(state, message).into() - } - - fn view<'a>( - &self, - state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state).into() - } - } - - Program { - raw: Application { - update, - view, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - }, - settings: Settings::default(), - } - .title(title) -} - -/// The underlying definition and configuration of an iced application. -/// -/// You can use this API to create and run iced applications -/// step by step—without coupling your logic to a trait -/// or a specific type. -/// -/// You can create a [`Program`] with the [`program`] helper. -/// -/// [`run`]: Program::run -#[derive(Debug)] -pub struct Program<P: Definition> { - raw: P, - settings: Settings, -} - -impl<P: Definition> Program<P> { - /// Runs the underlying [`Application`] of the [`Program`]. - /// - /// The state of the [`Program`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with - pub fn run(self) -> Result - where - Self: 'static, - P::State: Default, - { - self.run_with(P::State::default) - } - - /// Runs the underlying [`Application`] of the [`Program`] with a - /// closure that creates the initial state. - pub fn run_with( - self, - initialize: impl Fn() -> P::State + Clone + 'static, - ) -> Result - where - Self: 'static, - { - use std::marker::PhantomData; - - struct Instance<P: Definition, I> { - program: P, - state: P::State, - _initialize: PhantomData<I>, - } - - impl<P: Definition, I: Fn() -> P::State> Application for Instance<P, I> { - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Flags = (P, I); - type Executor = P::Executor; - - fn new( - (program, initialize): Self::Flags, - ) -> (Self, Task<Self::Message>) { - let state = initialize(); - let command = program.load(); - - ( - Self { - program, - state, - _initialize: PhantomData, - }, - command, - ) - } - - fn title(&self) -> String { - self.program.title(&self.state) - } - - fn update( - &mut self, - message: Self::Message, - ) -> Task<Self::Message> { - self.program.update(&mut self.state, message) - } - - fn view( - &self, - ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> - { - self.program.view(&self.state) - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.program.subscription(&self.state) - } - - fn theme(&self) -> Self::Theme { - self.program.theme(&self.state) - } - - fn style(&self, theme: &Self::Theme) -> Appearance { - self.program.style(&self.state, theme) - } - } - - let Self { raw, settings } = self; - - Instance::run(Settings { - flags: (raw, initialize), - id: settings.id, - window: settings.window, - fonts: settings.fonts, - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - }) - } - - /// Sets the [`Settings`] that will be used to run the [`Program`]. - pub fn settings(self, settings: Settings) -> Self { - Self { settings, ..self } - } - - /// Sets the [`Settings::antialiasing`] of the [`Program`]. - pub fn antialiasing(self, antialiasing: bool) -> Self { - Self { - settings: Settings { - antialiasing, - ..self.settings - }, - ..self - } - } - - /// Sets the default [`Font`] of the [`Program`]. - pub fn default_font(self, default_font: Font) -> Self { - Self { - settings: Settings { - default_font, - ..self.settings - }, - ..self - } - } - - /// Adds a font to the list of fonts that will be loaded at the start of the [`Program`]. - pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { - self.settings.fonts.push(font.into()); - self - } - - /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Program`]. - pub fn centered(self) -> Self { - Self { - settings: Settings { - window: window::Settings { - position: window::Position::Centered, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::exit_on_close_request`] of the [`Program`]. - pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { - Self { - settings: Settings { - window: window::Settings { - exit_on_close_request, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::size`] of the [`Program`]. - pub fn window_size(self, size: impl Into<Size>) -> Self { - Self { - settings: Settings { - window: window::Settings { - size: size.into(), - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::transparent`] of the [`Program`]. - pub fn transparent(self, transparent: bool) -> Self { - Self { - settings: Settings { - window: window::Settings { - transparent, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`Title`] of the [`Program`]. - pub(crate) fn title( - self, - title: impl Title<P::State>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_title(self.raw, title), - settings: self.settings, - } - } - - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_load(self.raw, f), - settings: self.settings, - } - } - - /// Sets the subscription logic of the [`Program`]. - pub fn subscription( - self, - f: impl Fn(&P::State) -> Subscription<P::Message>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_subscription(self.raw, f), - settings: self.settings, - } - } - - /// Sets the theme logic of the [`Program`]. - pub fn theme( - self, - f: impl Fn(&P::State) -> P::Theme, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_theme(self.raw, f), - settings: self.settings, - } - } - - /// Sets the style logic of the [`Program`]. - pub fn style( - self, - f: impl Fn(&P::State, &P::Theme) -> Appearance, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_style(self.raw, f), - settings: self.settings, - } - } -} +pub use crate::shell::program::{Appearance, DefaultStyle}; /// The internal definition of a [`Program`]. /// /// You should not need to implement this trait directly. Instead, use the /// methods available in the [`Program`] struct. #[allow(missing_docs)] -pub trait Definition: Sized { +pub trait Program: Sized { /// The state of the program. type State; @@ -442,9 +38,10 @@ pub trait Definition: Sized { fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; - fn title(&self, _state: &Self::State) -> String { + fn title(&self, _state: &Self::State, _window: window::Id) -> String { String::from("A cool iced application!") } @@ -455,28 +52,159 @@ pub trait Definition: Sized { Subscription::none() } - fn theme(&self, _state: &Self::State) -> Self::Theme { + fn theme(&self, _state: &Self::State, _window: window::Id) -> Self::Theme { Self::Theme::default() } fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance { DefaultStyle::default_style(theme) } + + fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 { + 1.0 + } + + /// Runs the [`Program`]. + /// + /// The state of the [`Program`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + fn run( + self, + settings: Settings, + window_settings: Option<window::Settings>, + ) -> Result + where + Self: 'static, + Self::State: Default, + { + self.run_with(settings, window_settings, Self::State::default) + } + + /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. + fn run_with<I>( + self, + settings: Settings, + window_settings: Option<window::Settings>, + initialize: I, + ) -> Result + where + Self: 'static, + I: Fn() -> Self::State + Clone + 'static, + { + use std::marker::PhantomData; + + struct Instance<P: Program, I> { + program: P, + state: P::State, + _initialize: PhantomData<I>, + } + + impl<P: Program, I: Fn() -> P::State> shell::Program for Instance<P, I> { + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Flags = (P, I); + type Executor = P::Executor; + + fn new( + (program, initialize): Self::Flags, + ) -> (Self, Task<Self::Message>) { + let state = initialize(); + let command = program.load(); + + ( + Self { + program, + state, + _initialize: PhantomData, + }, + command, + ) + } + + fn title(&self, window: window::Id) -> String { + self.program.title(&self.state, window) + } + + fn update( + &mut self, + message: Self::Message, + ) -> Task<Self::Message> { + self.program.update(&mut self.state, message) + } + + fn view( + &self, + window: window::Id, + ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> + { + self.program.view(&self.state, window) + } + + fn subscription(&self) -> Subscription<Self::Message> { + self.program.subscription(&self.state) + } + + fn theme(&self, window: window::Id) -> Self::Theme { + self.program.theme(&self.state, window) + } + + fn style(&self, theme: &Self::Theme) -> Appearance { + self.program.style(&self.state, theme) + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.program.scale_factor(&self.state, window) + } + } + + #[allow(clippy::needless_update)] + let renderer_settings = crate::graphics::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(crate::graphics::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::graphics::Settings::default() + }; + + Ok(shell::program::run::< + Instance<Self, I>, + <Self::Renderer as compositor::Default>::Compositor, + >( + Settings { + id: settings.id, + fonts: settings.fonts, + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + } + .into(), + renderer_settings, + window_settings, + (self, initialize), + )?) + } } -fn with_title<P: Definition>( +pub fn with_title<P: Program>( program: P, - title: impl Title<P::State>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + title: impl Fn(&P::State, window::Id) -> String, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithTitle<P, Title> { program: P, title: Title, } - impl<P, Title> Definition for WithTitle<P, Title> + impl<P, Title> Program for WithTitle<P, Title> where - P: Definition, - Title: self::Title<P::State>, + P: Program, + Title: Fn(&P::State, window::Id) -> String, { type State = P::State; type Message = P::Message; @@ -488,8 +216,8 @@ fn with_title<P: Definition>( self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.title.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + (self.title)(state, window) } fn update( @@ -503,12 +231,17 @@ fn with_title<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn subscription( @@ -525,21 +258,25 @@ fn with_title<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithTitle { program, title } } -fn with_load<P: Definition>( +pub fn with_load<P: Program>( program: P, f: impl Fn() -> Task<P::Message>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithLoad<P, F> { program: P, load: F, } - impl<P: Definition, F> Definition for WithLoad<P, F> + impl<P: Program, F> Program for WithLoad<P, F> where F: Fn() -> Task<P::Message>, { @@ -547,7 +284,7 @@ fn with_load<P: Definition>( type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; - type Executor = executor::Default; + type Executor = P::Executor; fn load(&self) -> Task<Self::Message> { Task::batch([self.program.load(), (self.load)()]) @@ -564,12 +301,13 @@ fn with_load<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn subscription( @@ -579,8 +317,12 @@ fn with_load<P: Definition>( self.program.subscription(state) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn style( @@ -590,21 +332,25 @@ fn with_load<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithLoad { program, load: f } } -fn with_subscription<P: Definition>( +pub fn with_subscription<P: Program>( program: P, f: impl Fn(&P::State) -> Subscription<P::Message>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithSubscription<P, F> { program: P, subscription: F, } - impl<P: Definition, F> Definition for WithSubscription<P, F> + impl<P: Program, F> Program for WithSubscription<P, F> where F: Fn(&P::State) -> Subscription<P::Message>, { @@ -612,7 +358,7 @@ fn with_subscription<P: Definition>( type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; - type Executor = executor::Default; + type Executor = P::Executor; fn subscription( &self, @@ -636,16 +382,21 @@ fn with_subscription<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn style( @@ -655,6 +406,10 @@ fn with_subscription<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithSubscription { @@ -663,18 +418,18 @@ fn with_subscription<P: Definition>( } } -fn with_theme<P: Definition>( +pub fn with_theme<P: Program>( program: P, - f: impl Fn(&P::State) -> P::Theme, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + f: impl Fn(&P::State, window::Id) -> P::Theme, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithTheme<P, F> { program: P, theme: F, } - impl<P: Definition, F> Definition for WithTheme<P, F> + impl<P: Program, F> Program for WithTheme<P, F> where - F: Fn(&P::State) -> P::Theme, + F: Fn(&P::State, window::Id) -> P::Theme, { type State = P::State; type Message = P::Message; @@ -682,16 +437,20 @@ fn with_theme<P: Definition>( type Renderer = P::Renderer; type Executor = P::Executor; - fn theme(&self, state: &Self::State) -> Self::Theme { - (self.theme)(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + (self.theme)(state, window) } fn load(&self) -> Task<Self::Message> { self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn update( @@ -705,8 +464,9 @@ fn with_theme<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } fn subscription( @@ -723,21 +483,25 @@ fn with_theme<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithTheme { program, theme: f } } -fn with_style<P: Definition>( +pub fn with_style<P: Program>( program: P, f: impl Fn(&P::State, &P::Theme) -> Appearance, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithStyle<P, F> { program: P, style: F, } - impl<P: Definition, F> Definition for WithStyle<P, F> + impl<P: Program, F> Program for WithStyle<P, F> where F: Fn(&P::State, &P::Theme) -> Appearance, { @@ -759,8 +523,8 @@ fn with_style<P: Definition>( self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn update( @@ -774,8 +538,9 @@ fn with_style<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } fn subscription( @@ -785,91 +550,96 @@ fn with_style<P: Definition>( self.program.subscription(state) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) } } WithStyle { program, style: f } } -/// The title logic of some [`Program`]. -/// -/// This trait is implemented both for `&static str` and -/// any closure `Fn(&State) -> String`. -/// -/// This trait allows the [`program`] builder to take any of them. -pub trait Title<State> { - /// Produces the title of the [`Program`]. - fn title(&self, state: &State) -> String; -} - -impl<State> Title<State> for &'static str { - fn title(&self, _state: &State) -> String { - self.to_string() +pub fn with_scale_factor<P: Program>( + program: P, + f: impl Fn(&P::State, window::Id) -> f64, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithScaleFactor<P, F> { + program: P, + scale_factor: F, } -} -impl<T, State> Title<State> for T -where - T: Fn(&State) -> String, -{ - fn title(&self, state: &State) -> String { - self(state) + impl<P: Program, F> Program for WithScaleFactor<P, F> + where + F: Fn(&P::State, window::Id) -> f64, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn load(&self) -> Task<Self::Message> { + self.program.load() + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state, window) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + (self.scale_factor)(state, window) + } } -} -/// The update logic of some [`Program`]. -/// -/// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Task<Message>>`. -pub trait Update<State, Message> { - /// Processes the message and updates the state of the [`Program`]. - fn update( - &self, - state: &mut State, - message: Message, - ) -> impl Into<Task<Message>>; -} - -impl<T, State, Message, C> Update<State, Message> for T -where - T: Fn(&mut State, Message) -> C, - C: Into<Task<Message>>, -{ - fn update( - &self, - state: &mut State, - message: Message, - ) -> impl Into<Task<Message>> { - self(state, message) - } -} - -/// The view logic of some [`Program`]. -/// -/// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Element<'_, Message>>`. -pub trait View<'a, State, Message, Theme, Renderer> { - /// Produces the widget of the [`Program`]. - fn view( - &self, - state: &'a State, - ) -> impl Into<Element<'a, Message, Theme, Renderer>>; -} - -impl<'a, T, State, Message, Theme, Renderer, Widget> - View<'a, State, Message, Theme, Renderer> for T -where - T: Fn(&'a State) -> Widget, - State: 'static, - Widget: Into<Element<'a, Message, Theme, Renderer>>, -{ - fn view( - &self, - state: &'a State, - ) -> impl Into<Element<'a, Message, Theme, Renderer>> { - self(state) + WithScaleFactor { + program, + scale_factor: f, } } diff --git a/src/settings.rs b/src/settings.rs index f7947841..ebac7a86 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,30 +1,17 @@ //! Configure your application. -use crate::window; use crate::{Font, Pixels}; use std::borrow::Cow; -/// The settings of an iced [`Program`]. -/// -/// [`Program`]: crate::Program +/// The settings of an iced program. #[derive(Debug, Clone)] -pub struct Settings<Flags = ()> { +pub struct Settings { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or /// communicate with it through the windowing system. pub id: Option<String>, - /// The window settings. - /// - /// They will be ignored on the Web. - pub window: window::Settings, - - /// The data needed to initialize the [`Program`]. - /// - /// [`Program`]: crate::Program - pub flags: Flags, - /// The fonts to load on boot. pub fonts: Vec<Cow<'static, [u8]>>, @@ -50,34 +37,10 @@ pub struct Settings<Flags = ()> { pub antialiasing: bool, } -impl<Flags> Settings<Flags> { - /// Initialize [`Program`] settings using the given data. - /// - /// [`Program`]: crate::Program - pub fn with_flags(flags: Flags) -> Self { - let default_settings = Settings::<()>::default(); - - Self { - flags, - id: default_settings.id, - window: default_settings.window, - fonts: default_settings.fonts, - default_font: default_settings.default_font, - default_text_size: default_settings.default_text_size, - antialiasing: default_settings.antialiasing, - } - } -} - -impl<Flags> Default for Settings<Flags> -where - Flags: Default, -{ +impl Default for Settings { fn default() -> Self { Self { id: None, - window: window::Settings::default(), - flags: Default::default(), fonts: Vec::new(), default_font: Font::default(), default_text_size: Pixels(16.0), @@ -86,12 +49,10 @@ where } } -impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { - fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { +impl From<Settings> for iced_winit::Settings { + fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { id: settings.id, - window: settings.window, - flags: settings.flags, fonts: settings.fonts, } } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 6d3dddde..68368aa1 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -17,7 +17,7 @@ workspace = true default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] debug = ["iced_runtime/debug"] system = ["sysinfo"] -application = [] +program = [] x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] diff --git a/winit/src/application.rs b/winit/src/application.rs deleted file mode 100644 index a93878ea..00000000 --- a/winit/src/application.rs +++ /dev/null @@ -1,1082 +0,0 @@ -//! Create interactive, native cross-platform applications. -mod state; - -pub use state::State; - -use crate::conversion; -use crate::core; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::time::Instant; -use crate::core::widget::operation; -use crate::core::window; -use crate::core::{Color, Event, Point, Size, Theme}; -use crate::futures::futures; -use crate::futures::subscription::{self, Subscription}; -use crate::futures::{Executor, Runtime}; -use crate::graphics; -use crate::graphics::compositor::{self, Compositor}; -use crate::runtime::clipboard; -use crate::runtime::program::Program; -use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Action, Debug, Task}; -use crate::{Clipboard, Error, Proxy, Settings}; - -use futures::channel::mpsc; -use futures::channel::oneshot; - -use std::borrow::Cow; -use std::mem::ManuallyDrop; -use std::sync::Arc; - -/// An interactive, native cross-platform application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`]. It will run in -/// its own window. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Task`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -pub trait Application: Program -where - Self::Theme: DefaultStyle, -{ - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); - - /// Returns the current title of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self) -> String; - - /// Returns the current `Theme` of the [`Application`]. - fn theme(&self) -> Self::Theme; - - /// Returns the `Style` variation of the `Theme`. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() - } - - /// Returns the event `Subscription` for the current state of the - /// application. - /// - /// The messages produced by the `Subscription` will be handled by - /// [`update`](#tymethod.update). - /// - /// A `Subscription` will be kept alive as long as you keep returning it! - /// - /// By default, it returns an empty subscription. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { - 1.0 - } -} - -/// The appearance of an application. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { - /// The background [`Color`] of the application. - pub background_color: Color, - - /// The default text [`Color`] of the application. - pub text_color: Color, -} - -/// The default style of an [`Application`]. -pub trait DefaultStyle { - /// Returns the default style of an [`Application`]. - fn default_style(&self) -> Appearance; -} - -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) - } -} - -/// The default [`Appearance`] of an [`Application`] with the built-in [`Theme`]. -pub fn default(theme: &Theme) -> Appearance { - let palette = theme.extended_palette(); - - Appearance { - background_color: palette.background.base.color, - text_color: palette.background.base.text, - } -} - -/// Runs an [`Application`] with an executor, compositor, and the provided -/// settings. -pub fn run<A, E, C>( - settings: Settings<A::Flags>, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use futures::task; - use futures::Future; - use winit::event_loop::EventLoop; - - let mut debug = Debug::new(); - debug.startup_started(); - - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); - - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - - let mut runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - executor.spawn(worker); - - Runtime::new(executor, proxy.clone()) - }; - - let (application, task) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - if let Some(stream) = task.into_stream() { - runtime.run(stream); - } - - let id = settings.id; - let title = application.title(); - - let (boot_sender, boot_receiver) = oneshot::channel(); - let (event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, control_receiver) = mpsc::unbounded(); - - let instance = Box::pin(run_instance::<A, E, C>( - application, - runtime, - proxy, - debug, - boot_receiver, - event_receiver, - control_sender, - settings.fonts, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner<Message: 'static, F, C> { - instance: std::pin::Pin<Box<F>>, - context: task::Context<'static>, - boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<winit::event::Event<Action<Message>>>, - receiver: mpsc::UnboundedReceiver<winit::event_loop::ControlFlow>, - error: Option<Error>, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc<std::cell::RefCell<bool>>, - #[cfg(target_arch = "wasm32")] - queued_events: Vec<winit::event::Event<Action<Message>>>, - } - - struct BootConfig<C> { - sender: oneshot::Sender<Boot<C>>, - id: Option<String>, - title: String, - window_settings: window::Settings, - graphics_settings: graphics::Settings, - } - - let runner = Runner { - instance, - context, - boot: Some(BootConfig { - sender: boot_sender, - id, - title, - window_settings: settings.window, - graphics_settings, - }), - sender: event_sender, - receiver: control_receiver, - error: None, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), - #[cfg(target_arch = "wasm32")] - queued_events: Vec::new(), - }; - - impl<Message, F, C> winit::application::ApplicationHandler<Action<Message>> - for Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor + 'static, - { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let Some(BootConfig { - sender, - id, - title, - window_settings, - graphics_settings, - }) = self.boot.take() - else { - return; - }; - - let should_be_visible = window_settings.visible; - let exit_on_close_request = window_settings.exit_on_close_request; - - #[cfg(target_arch = "wasm32")] - let target = window_settings.platform_specific.target.clone(); - - let window_attributes = conversion::window_attributes( - window_settings, - &title, - event_loop.primary_monitor(), - id, - ) - .with_visible(false); - - log::debug!("Window attributes: {window_attributes:#?}"); - - let window = match event_loop.create_window(window_attributes) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; - } - }; - - let finish_boot = { - let window = window.clone(); - - async move { - let compositor = - C::new(graphics_settings, window.clone()).await?; - - sender - .send(Boot { - window, - compositor, - should_be_visible, - exit_on_close_request, - }) - .ok() - .expect("Send boot event"); - - Ok::<_, graphics::Error>(()) - } - }; - - #[cfg(not(target_arch = "wasm32"))] - if let Err(error) = futures::executor::block_on(finish_boot) { - self.error = Some(Error::GraphicsCreationFailed(error)); - event_loop.exit(); - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = window.canvas().expect("Get window canvas"); - let _ = canvas.set_attribute( - "style", - "display: block; width: 100%; height: 100%", - ); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{target}")) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node.replace_with_with_node_1(&canvas).expect( - &format!("Could not replace #{}", node.id()), - ); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - - let is_booted = self.is_booted.clone(); - - wasm_bindgen_futures::spawn_local(async move { - finish_boot.await.expect("Finish boot!"); - - *is_booted.borrow_mut() = true; - }); - } - } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - if self.boot.is_some() { - return; - } - - self.process_event( - event_loop, - winit::event::Event::NewEvents(cause), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - winit::event::Event::WindowEvent { window_id, event }, - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - winit::event::Event::AboutToWait, - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - action: Action<Message>, - ) { - self.process_event( - event_loop, - winit::event::Event::UserEvent(action), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - self.process_event(event_loop, winit::event::Event::AboutToWait); - } - } - - impl<Message, F, C> Runner<Message, F, C> - where - F: Future<Output = ()>, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::Event<Action<Message>>, - ) { - // On Wasm, events may start being processed before the compositor - // boots up. We simply queue them and process them once ready. - #[cfg(target_arch = "wasm32")] - if !*self.is_booted.borrow() { - self.queued_events.push(event); - return; - } else if !self.queued_events.is_empty() { - let queued_events = std::mem::take(&mut self.queued_events); - - // This won't infinitely recurse, since we `mem::take` - for event in queued_events { - self.process_event(event_loop, event); - } - } - - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = self.receiver.try_next() { - event_loop.set_control_flow(flow); - } - } - task::Poll::Ready(_) => { - event_loop.exit(); - } - } - } - } - - #[cfg(not(target_arch = "wasm32"))] - { - let mut runner = runner; - let _ = event_loop.run_app(&mut runner); - - runner.error.map(Err).unwrap_or(Ok(())) - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::EventLoopExtWebSys; - let _ = event_loop.spawn_app(runner); - - Ok(()) - } -} - -struct Boot<C> { - window: Arc<winit::window::Window>, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -async fn run_instance<A, E, C>( - mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, Action<A::Message>>, - mut proxy: Proxy<A::Message>, - mut debug: Debug, - mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<Action<A::Message>>, - >, - mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, - fonts: Vec<Cow<'static, [u8]>>, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use futures::stream::StreamExt; - use winit::event; - use winit::event_loop::ControlFlow; - - let Boot { - window, - mut compositor, - should_be_visible, - exit_on_close_request, - } = boot.try_recv().ok().flatten().expect("Receive boot"); - - let mut renderer = compositor.create_renderer(); - - for font in fonts { - compositor.load_font(font); - } - - let mut state = State::new(&application, &window); - let mut viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - - let mut clipboard = Clipboard::connect(&window); - let cache = user_interface::Cache::default(); - let mut surface = compositor.create_surface( - window.clone(), - physical_size.width, - physical_size.height, - ); - let mut should_exit = false; - - if should_be_visible { - window.set_visible(true); - } - - runtime.track( - application - .subscription() - .map(Action::Output) - .into_recipes(), - ); - - let mut user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); - let mut messages = Vec::new(); - let mut user_events = 0; - let mut redraw_pending = false; - - debug.startup_finished(); - - while let Some(event) = event_receiver.next().await { - match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) if !redraw_pending => { - window.request_redraw(); - redraw_pending = true; - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - runtime.broadcast(subscription::Event::PlatformSpecific( - subscription::PlatformSpecific::MacOS( - subscription::MacOS::ReceivedUrl(url), - ), - )); - } - event::Event::UserEvent(action) => { - run_action( - action, - &mut user_interface, - &mut compositor, - &mut surface, - &state, - &mut renderer, - &mut messages, - &mut clipboard, - &mut should_exit, - &mut debug, - &window, - ); - - user_events += 1; - } - event::Event::WindowEvent { - event: event::WindowEvent::RedrawRequested { .. }, - .. - } => { - let physical_size = state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 { - continue; - } - - let current_viewport_version = state.viewport_version(); - - if viewport_version != current_viewport_version { - let logical_size = state.logical_size(); - - debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - debug.layout_finished(); - - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); - - viewport_version = current_viewport_version; - } - - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = Event::Window( - window::Event::RedrawRequested(Instant::now()), - ); - - let (interface_state, _) = user_interface.update( - &[redraw_event.clone()], - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - window.request_redraw(); - - ControlFlow::Wait - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - runtime.broadcast(subscription::Event::Interaction { - window: window::Id::MAIN, - event: redraw_event, - status: core::event::Status::Ignored, - }); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor(), - ); - redraw_pending = false; - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - - debug.render_started(); - match compositor.present( - &mut renderer, - &mut surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ) { - Ok(()) => { - debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{error:?}"); - } - _ => { - debug.render_finished(); - - // Try rendering again next frame. - window.request_redraw(); - } - }, - } - } - event::Event::WindowEvent { - event: window_event, - .. - } => { - if requests_exit(&window_event, state.modifiers()) - && exit_on_close_request - { - break; - } - - state.update(&window, &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(event); - } - } - event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - debug.event_processing_finished(); - - for (event, status) in - events.drain(..).zip(statuses.into_iter()) - { - runtime.broadcast(subscription::Event::Interaction { - window: window::Id::MAIN, - event, - status, - }); - } - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - update( - &mut application, - &mut state, - &mut runtime, - &mut debug, - &mut messages, - &window, - ); - - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } - - if user_events > 0 { - proxy.free_slots(user_events); - user_events = 0; - } - } - - if !redraw_pending { - window.request_redraw(); - redraw_pending = true; - } - } - _ => {} - } - } - - // Manually drop the user interface - drop(ManuallyDrop::into_inner(user_interface)); -} - -/// Returns true if the provided event should cause an [`Application`] to -/// exit. -pub fn requests_exit( - event: &winit::event::WindowEvent, - _modifiers: winit::keyboard::ModifiersState, -) -> bool { - use winit::event::WindowEvent; - - match event { - WindowEvent::CloseRequested => true, - #[cfg(target_os = "macos")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Character(c), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if c == "q" && _modifiers.super_key() => true, - _ => false, - } -} - -/// Builds a [`UserInterface`] for the provided [`Application`], logging -/// [`struct@Debug`] information accordingly. -pub fn build_user_interface<'a, A: Application>( - application: &'a A, - cache: user_interface::Cache, - renderer: &mut A::Renderer, - size: Size, - debug: &mut Debug, -) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> -where - A::Theme: DefaultStyle, -{ - debug.view_started(); - let view = application.view(); - debug.view_finished(); - - debug.layout_started(); - let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); - - user_interface -} - -/// Updates an [`Application`] by feeding it the provided messages, spawning any -/// resulting [`Task`], and tracking its [`Subscription`]. -pub fn update<A: Application, E: Executor>( - application: &mut A, - state: &mut State<A>, - runtime: &mut Runtime<E, Proxy<A::Message>, Action<A::Message>>, - debug: &mut Debug, - messages: &mut Vec<A::Message>, - window: &winit::window::Window, -) where - A::Theme: DefaultStyle, -{ - for message in messages.drain(..) { - debug.log_message(&message); - - debug.update_started(); - let task = runtime.enter(|| application.update(message)); - debug.update_finished(); - - if let Some(stream) = task.into_stream() { - runtime.run(stream); - } - } - - state.synchronize(application, window); - - let subscription = application.subscription(); - runtime.track(subscription.map(Action::Output).into_recipes()); -} - -/// Runs the actions of a [`Task`]. -pub fn run_action<A, C>( - action: Action<A::Message>, - user_interface: &mut UserInterface<'_, A::Message, A::Theme, C::Renderer>, - compositor: &mut C, - surface: &mut C::Surface, - state: &State<A>, - renderer: &mut A::Renderer, - messages: &mut Vec<A::Message>, - clipboard: &mut Clipboard, - should_exit: &mut bool, - debug: &mut Debug, - window: &winit::window::Window, -) where - A: Application, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use crate::runtime::system; - use crate::runtime::window; - - match action { - Action::Clipboard(action) => match action { - clipboard::Action::Read { target, channel } => { - let _ = channel.send(clipboard.read(target)); - } - clipboard::Action::Write { target, contents } => { - clipboard.write(target, contents); - } - }, - Action::Window(action) => match action { - window::Action::Close(_id) => { - *should_exit = true; - } - window::Action::Drag(_id) => { - let _res = window.drag_window(); - } - window::Action::Open { .. } => { - log::warn!( - "Spawning a window is only available with \ - multi-window applications." - ); - } - window::Action::Resize(_id, size) => { - let _ = window.request_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); - } - window::Action::FetchSize(_id, channel) => { - let size = - window.inner_size().to_logical(window.scale_factor()); - - let _ = channel.send(Size::new(size.width, size.height)); - } - window::Action::FetchMaximized(_id, channel) => { - let _ = channel.send(window.is_maximized()); - } - window::Action::Maximize(_id, maximized) => { - window.set_maximized(maximized); - } - window::Action::FetchMinimized(_id, channel) => { - let _ = channel.send(window.is_minimized()); - } - window::Action::Minimize(_id, minimized) => { - window.set_minimized(minimized); - } - window::Action::FetchPosition(_id, channel) => { - let position = window - .inner_position() - .map(|position| { - let position = - position.to_logical::<f32>(window.scale_factor()); - - Point::new(position.x, position.y) - }) - .ok(); - - let _ = channel.send(position); - } - window::Action::Move(_id, position) => { - window.set_outer_position(winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }); - } - window::Action::ChangeMode(_id, mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - mode, - )); - } - window::Action::ChangeIcon(_id, icon) => { - window.set_window_icon(conversion::icon(icon)); - } - window::Action::FetchMode(_id, channel) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - let _ = channel.send(mode); - } - window::Action::ToggleMaximize(_id) => { - window.set_maximized(!window.is_maximized()); - } - window::Action::ToggleDecorations(_id) => { - window.set_decorations(!window.is_decorated()); - } - window::Action::RequestUserAttention(_id, user_attention) => { - window.request_user_attention( - user_attention.map(conversion::user_attention), - ); - } - window::Action::GainFocus(_id) => { - window.focus_window(); - } - window::Action::ChangeLevel(_id, level) => { - window.set_window_level(conversion::window_level(level)); - } - window::Action::ShowSystemMenu(_id) => { - if let mouse::Cursor::Available(point) = state.cursor() { - window.show_window_menu(winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }); - } - } - window::Action::FetchRawId(_id, channel) => { - let _ = channel.send(window.id().into()); - } - window::Action::RunWithHandle(_id, f) => { - use window::raw_window_handle::HasWindowHandle; - - if let Ok(handle) = window.window_handle() { - f(handle); - } - } - - window::Action::Screenshot(_id, channel) => { - let bytes = compositor.screenshot( - renderer, - surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - let _ = channel.send(window::Screenshot::new( - bytes, - state.physical_size(), - state.viewport().scale_factor(), - )); - } - }, - Action::System(action) => match action { - system::Action::QueryInformation(_channel) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let _ = _channel.send(information); - }); - } - } - }, - Action::Widget(operation) => { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - } - Action::LoadFont { bytes, channel } => { - // TODO: Error handling (?) - compositor.load_font(bytes); - - let _ = channel.send(Ok(())); - } - Action::Output(message) => { - messages.push(message); - } - } -} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs deleted file mode 100644 index a0a06933..00000000 --- a/winit/src/application/state.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::application; -use crate::conversion; -use crate::core::mouse; -use crate::core::{Color, Size}; -use crate::graphics::Viewport; -use crate::runtime::Debug; -use crate::Application; - -use std::marker::PhantomData; -use winit::event::{Touch, WindowEvent}; -use winit::window::Window; - -/// The state of a windowed [`Application`]. -#[allow(missing_debug_implementations)] -pub struct State<A: Application> -where - A::Theme: application::DefaultStyle, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_version: usize, - cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: application::Appearance, - application: PhantomData<A>, -} - -impl<A: Application> State<A> -where - A::Theme: application::DefaultStyle, -{ - /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window: &Window) -> Self { - let title = application.title(); - let scale_factor = application.scale_factor(); - let theme = application.theme(); - let appearance = application.style(&theme); - - let viewport = { - let physical_size = window.inner_size(); - - Viewport::with_physical_size( - Size::new(physical_size.width, physical_size.height), - window.scale_factor() * scale_factor, - ) - }; - - Self { - title, - scale_factor, - viewport, - viewport_version: 0, - cursor_position: None, - modifiers: winit::keyboard::ModifiersState::default(), - theme, - appearance, - application: PhantomData, - } - } - - /// Returns the current [`Viewport`] of the [`State`]. - pub fn viewport(&self) -> &Viewport { - &self.viewport - } - - /// Returns the version of the [`Viewport`] of the [`State`]. - /// - /// The version is incremented every time the [`Viewport`] changes. - pub fn viewport_version(&self) -> usize { - self.viewport_version - } - - /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn physical_size(&self) -> Size<u32> { - self.viewport.physical_size() - } - - /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn logical_size(&self) -> Size<f32> { - self.viewport.logical_size() - } - - /// Returns the current scale factor of the [`Viewport`] of the [`State`]. - pub fn scale_factor(&self) -> f64 { - self.viewport.scale_factor() - } - - /// Returns the current cursor position of the [`State`]. - pub fn cursor(&self) -> mouse::Cursor { - self.cursor_position - .map(|cursor_position| { - conversion::cursor_position( - cursor_position, - self.viewport.scale_factor(), - ) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable) - } - - /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> winit::keyboard::ModifiersState { - self.modifiers - } - - /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &A::Theme { - &self.theme - } - - /// Returns the current background [`Color`] of the [`State`]. - pub fn background_color(&self) -> Color { - self.appearance.background_color - } - - /// Returns the current text [`Color`] of the [`State`]. - pub fn text_color(&self) -> Color { - self.appearance.text_color - } - - /// Processes the provided window event and updates the [`State`] - /// accordingly. - pub fn update( - &mut self, - window: &Window, - event: &WindowEvent, - _debug: &mut Debug, - ) { - match event { - WindowEvent::Resized(new_size) => { - let size = Size::new(new_size.width, new_size.height); - - self.viewport = Viewport::with_physical_size( - size, - window.scale_factor() * self.scale_factor, - ); - - self.viewport_version = self.viewport_version.wrapping_add(1); - } - WindowEvent::ScaleFactorChanged { - scale_factor: new_scale_factor, - .. - } => { - let size = self.viewport.physical_size(); - - self.viewport = Viewport::with_physical_size( - size, - new_scale_factor * self.scale_factor, - ); - - self.viewport_version = self.viewport_version.wrapping_add(1); - } - WindowEvent::CursorMoved { position, .. } - | WindowEvent::Touch(Touch { - location: position, .. - }) => { - self.cursor_position = Some(*position); - } - WindowEvent::CursorLeft { .. } => { - self.cursor_position = None; - } - WindowEvent::ModifiersChanged(new_modifiers) => { - self.modifiers = new_modifiers.state(); - } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => _debug.toggle(), - _ => {} - } - } - - /// Synchronizes the [`State`] with its [`Application`] and its respective - /// window. - /// - /// Normally an [`Application`] should be synchronized with its [`State`] - /// and window after calling [`crate::application::update`]. - pub fn synchronize(&mut self, application: &A, window: &Window) { - // Update window title - let new_title = application.title(); - - if self.title != new_title { - window.set_title(&new_title); - - self.title = new_title; - } - - // Update scale factor and size - let new_scale_factor = application.scale_factor(); - let new_size = window.inner_size(); - let current_size = self.viewport.physical_size(); - - if self.scale_factor != new_scale_factor - || (current_size.width, current_size.height) - != (new_size.width, new_size.height) - { - self.viewport = Viewport::with_physical_size( - Size::new(new_size.width, new_size.height), - window.scale_factor() * new_scale_factor, - ); - self.viewport_version = self.viewport_version.wrapping_add(1); - - self.scale_factor = new_scale_factor; - } - - // Update theme and appearance - self.theme = application.theme(); - self.appearance = application.style(&self.theme); - } -} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 3619cde8..3c11b72a 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -5,7 +5,7 @@ //! `iced_winit` offers some convenient abstractions on top of [`iced_runtime`] //! to quickstart development when using [`winit`]. //! -//! It exposes a renderer-agnostic [`Application`] trait that can be implemented +//! It exposes a renderer-agnostic [`Program`] trait that can be implemented //! and then run with a simple call. The use of this trait is optional. //! //! Additionally, a [`conversion`] module is available for users that decide to @@ -24,24 +24,23 @@ pub use iced_runtime::core; pub use iced_runtime::futures; pub use winit; -#[cfg(feature = "multi-window")] -pub mod multi_window; - -#[cfg(feature = "application")] -pub mod application; pub mod clipboard; pub mod conversion; pub mod settings; +#[cfg(feature = "program")] +pub mod program; + #[cfg(feature = "system")] pub mod system; mod error; mod proxy; -#[cfg(feature = "application")] -pub use application::Application; pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; pub use settings::Settings; + +#[cfg(feature = "program")] +pub use program::Program; diff --git a/winit/src/multi_window.rs b/winit/src/program.rs similarity index 85% rename from winit/src/multi_window.rs rename to winit/src/program.rs index 8bd8a64d..28cd8e52 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/program.rs @@ -10,7 +10,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation; use crate::core::window; -use crate::core::{Point, Size}; +use crate::core::{Color, Element, Point, Size, Theme}; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; use crate::futures::futures::executor; @@ -20,14 +20,12 @@ use crate::futures::subscription::{self, Subscription}; use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::{compositor, Compositor}; -use crate::multi_window::window_manager::WindowManager; -use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; -use crate::runtime::{Action, Task}; +use crate::runtime::{self, Action, Task}; use crate::{Clipboard, Error, Proxy, Settings}; -pub use crate::application::{default, Appearance, DefaultStyle}; +use window_manager::WindowManager; use rustc_hash::FxHashMap; use std::mem::ManuallyDrop; @@ -40,19 +38,37 @@ use std::time::Instant; /// your GUI application by simply calling [`run`]. It will run in /// its own window. /// -/// An [`Application`] can execute asynchronous actions by returning a +/// A [`Program`] can execute asynchronous actions by returning a /// [`Task`] in some of its methods. /// -/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// When using a [`Program`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. -pub trait Application: Program +pub trait Program where + Self: Sized, Self::Theme: DefaultStyle, { - /// The data needed to initialize your [`Application`]. + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme used to draw the [`Program`]. + type Theme; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::futures::backend::default::Executor + type Executor: Executor; + + /// The graphics backend to use to draw the [`Program`]. + type Renderer: core::Renderer + core::text::Renderer; + + /// The data needed to initialize your [`Program`]. type Flags; - /// Initializes the [`Application`] with the flags provided to + /// Initializes the [`Program`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// /// Here is where you should return the initial state of your app. @@ -62,13 +78,31 @@ where /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); - /// Returns the current title of the [`Application`]. + /// Returns the current title of the [`Program`]. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. fn title(&self, window: window::Id) -> String; - /// Returns the current `Theme` of the [`Application`]. + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + + /// Returns the current `Theme` of the [`Program`]. fn theme(&self, window: window::Id) -> Self::Theme; /// Returns the `Style` variation of the `Theme`. @@ -89,7 +123,7 @@ where Subscription::none() } - /// Returns the scale factor of the window of the [`Application`]. + /// Returns the scale factor of the window of the [`Program`]. /// /// It can be used to dynamically control the size of the UI at runtime /// (i.e. zooming). @@ -104,17 +138,49 @@ where } } -/// Runs an [`Application`] with an executor, compositor, and the provided +/// The appearance of a program. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The background [`Color`] of the application. + pub background_color: Color, + + /// The default text [`Color`] of the application. + pub text_color: Color, +} + +/// The default style of a [`Program`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Program`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +/// The default [`Appearance`] of a [`Program`] with the built-in [`Theme`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background_color: palette.background.base.color, + text_color: palette.background.base.text, + } +} +/// Runs a [`Program`] with an executor, compositor, and the provided /// settings. -pub fn run<A, E, C>( - settings: Settings<A::Flags>, +pub fn run<P, C>( + settings: Settings, graphics_settings: graphics::Settings, + window_settings: Option<window::Settings>, + flags: P::Flags, ) -> Result<(), Error> where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, + P: Program + 'static, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: DefaultStyle, { use winit::event_loop::EventLoop; @@ -128,30 +194,24 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); let mut runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + let executor = + P::Executor::new().map_err(Error::ExecutorCreationFailed)?; executor.spawn(worker); Runtime::new(executor, proxy.clone()) }; - let (application, task) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; + let (application, task) = runtime.enter(|| P::new(flags)); if let Some(stream) = task.into_stream() { runtime.run(stream); } - let id = settings.id; - let title = application.title(window::Id::MAIN); - let (boot_sender, boot_receiver) = oneshot::channel(); let (event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, control_receiver) = mpsc::unbounded(); - let instance = Box::pin(run_instance::<A, E, C>( + let instance = Box::pin(run_instance::<P, C>( application, runtime, proxy, @@ -166,6 +226,7 @@ where struct Runner<Message: 'static, F, C> { instance: std::pin::Pin<Box<F>>, context: task::Context<'static>, + id: Option<String>, boot: Option<BootConfig<C>>, sender: mpsc::UnboundedSender<Event<Message>>, receiver: mpsc::UnboundedReceiver<Control>, @@ -174,20 +235,17 @@ where struct BootConfig<C> { sender: oneshot::Sender<Boot<C>>, - id: Option<String>, - title: String, - window_settings: window::Settings, + window_settings: Option<window::Settings>, graphics_settings: graphics::Settings, } let mut runner = Runner { instance, context, + id: settings.id, boot: Some(BootConfig { sender: boot_sender, - id, - title, - window_settings: settings.window, + window_settings, graphics_settings, }), sender: event_sender, @@ -204,8 +262,6 @@ where fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let Some(BootConfig { sender, - id, - title, window_settings, graphics_settings, }) = self.boot.take() @@ -213,20 +269,9 @@ where return; }; - let should_be_visible = window_settings.visible; - let exit_on_close_request = window_settings.exit_on_close_request; - - let window_attributes = conversion::window_attributes( - window_settings, - &title, - event_loop.primary_monitor(), - id, - ) - .with_visible(false); - - log::debug!("Window attributes: {window_attributes:#?}"); - - let window = match event_loop.create_window(window_attributes) { + let window = match event_loop.create_window( + winit::window::WindowAttributes::default().with_visible(false), + ) { Ok(window) => Arc::new(window), Err(error) => { self.error = Some(Error::WindowCreationFailed(error)); @@ -235,16 +280,17 @@ where } }; + let clipboard = Clipboard::connect(&window); + let finish_boot = async move { let compositor = C::new(graphics_settings, window.clone()).await?; sender .send(Boot { - window, compositor, - should_be_visible, - exit_on_close_request, + clipboard, + window_settings, }) .ok() .expect("Send boot event"); @@ -386,7 +432,12 @@ where let window = event_loop .create_window( conversion::window_attributes( - settings, &title, monitor, None, + settings, + &title, + monitor + .or(event_loop + .primary_monitor()), + self.id.clone(), ), ) .expect("Create window"); @@ -423,10 +474,9 @@ where } struct Boot<C> { - window: Arc<winit::window::Window>, compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, + clipboard: Clipboard, + window_settings: Option<window::Settings>, } enum Event<Message: 'static> { @@ -449,62 +499,37 @@ enum Control { }, } -async fn run_instance<A, E, C>( - mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, Action<A::Message>>, - mut proxy: Proxy<A::Message>, +async fn run_instance<P, C>( + mut program: P, + mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>, + mut proxy: Proxy<P::Message>, mut debug: Debug, mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver<Event<Action<A::Message>>>, + mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, mut control_sender: mpsc::UnboundedSender<Control>, ) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, + P: Program + 'static, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: DefaultStyle, { use winit::event; use winit::event_loop::ControlFlow; let Boot { - window: main_window, mut compositor, - should_be_visible, - exit_on_close_request, + mut clipboard, + window_settings, } = boot.try_recv().ok().flatten().expect("Receive boot"); let mut window_manager = WindowManager::new(); - let _ = window_manager.insert( - window::Id::MAIN, - main_window, - &application, - &mut compositor, - exit_on_close_request, - ); - - let main_window = window_manager - .get_mut(window::Id::MAIN) - .expect("Get main window"); - - if should_be_visible { - main_window.raw.set_visible(true); - } - - let mut clipboard = Clipboard::connect(&main_window.raw); - let mut events = { - vec![( - window::Id::MAIN, - core::Event::Window(window::Event::Opened { - position: main_window.position(), - size: main_window.size(), - }), - )] - }; + let mut events = Vec::new(); + let mut messages = Vec::new(); + let mut actions = 0; let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, + &program, &mut debug, &mut window_manager, FxHashMap::from_iter([( @@ -513,15 +538,19 @@ async fn run_instance<A, E, C>( )]), )); - runtime.track( - application - .subscription() - .map(Action::Output) - .into_recipes(), - ); + runtime.track(program.subscription().map(Action::Output).into_recipes()); - let mut messages = Vec::new(); - let mut user_events = 0; + let is_daemon = window_settings.is_none(); + + if let Some(window_settings) = window_settings { + let (sender, _receiver) = oneshot::channel(); + + proxy.send_action(Action::Window(runtime::window::Action::Open( + window::Id::unique(), + window_settings, + sender, + ))); + } debug.startup_finished(); @@ -535,7 +564,7 @@ async fn run_instance<A, E, C>( let window = window_manager.insert( id, Arc::new(window), - &application, + &program, &mut compositor, exit_on_close_request, ); @@ -545,7 +574,7 @@ async fn run_instance<A, E, C>( let _ = user_interfaces.insert( id, build_user_interface( - &application, + &program, user_interface::Cache::default(), &mut window.renderer, logical_size, @@ -591,7 +620,7 @@ async fn run_instance<A, E, C>( event::Event::UserEvent(action) => { run_action( action, - &application, + &program, &mut compositor, &mut messages, &mut clipboard, @@ -601,7 +630,7 @@ async fn run_instance<A, E, C>( &mut window_manager, &mut ui_caches, ); - user_events += 1; + actions += 1; } event::Event::WindowEvent { window_id: id, @@ -782,6 +811,16 @@ async fn run_instance<A, E, C>( event: window_event, window_id, } => { + if !is_daemon + && matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) + && window_manager.is_empty() + { + break 'main; + } + let Some((id, window)) = window_manager.get_mut_alias(window_id) else { @@ -801,10 +840,6 @@ async fn run_instance<A, E, C>( id, core::Event::Window(window::Event::Closed), )); - - if window_manager.is_empty() { - break 'main; - } } else { window.state.update( &window.raw, @@ -903,7 +938,7 @@ async fn run_instance<A, E, C>( // Update application update( - &mut application, + &mut program, &mut runtime, &mut debug, &mut messages, @@ -913,7 +948,7 @@ async fn run_instance<A, E, C>( // application update since we don't know what changed for (id, window) in window_manager.iter_mut() { window.state.synchronize( - &application, + &program, id, &window.raw, ); @@ -926,15 +961,15 @@ async fn run_instance<A, E, C>( // rebuild UIs with the synchronized states user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, + &program, &mut debug, &mut window_manager, cached_interfaces, )); - if user_events > 0 { - proxy.free_slots(user_events); - user_events = 0; + if actions > 0 { + proxy.free_slots(actions); + actions = 0; } } } @@ -947,17 +982,17 @@ async fn run_instance<A, E, C>( let _ = ManuallyDrop::into_inner(user_interfaces); } -/// Builds a window's [`UserInterface`] for the [`Application`]. -fn build_user_interface<'a, A: Application>( - application: &'a A, +/// Builds a window's [`UserInterface`] for the [`Program`]. +fn build_user_interface<'a, P: Program>( + application: &'a P, cache: user_interface::Cache, - renderer: &mut A::Renderer, + renderer: &mut P::Renderer, size: Size, debug: &mut Debug, id: window::Id, -) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> +) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> where - A::Theme: DefaultStyle, + P::Theme: DefaultStyle, { debug.view_started(); let view = application.view(id); @@ -970,13 +1005,13 @@ where user_interface } -fn update<A: Application, E: Executor>( - application: &mut A, - runtime: &mut Runtime<E, Proxy<A::Message>, Action<A::Message>>, +fn update<P: Program, E: Executor>( + application: &mut P, + runtime: &mut Runtime<E, Proxy<P::Message>, Action<P::Message>>, debug: &mut Debug, - messages: &mut Vec<A::Message>, + messages: &mut Vec<P::Message>, ) where - A::Theme: DefaultStyle, + P::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); @@ -994,24 +1029,24 @@ fn update<A: Application, E: Executor>( runtime.track(subscription.map(Action::Output).into_recipes()); } -fn run_action<A, C>( - action: Action<A::Message>, - application: &A, +fn run_action<P, C>( + action: Action<P::Message>, + application: &P, compositor: &mut C, - messages: &mut Vec<A::Message>, + messages: &mut Vec<P::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender<Control>, debug: &mut Debug, interfaces: &mut FxHashMap< window::Id, - UserInterface<'_, A::Message, A::Theme, A::Renderer>, + UserInterface<'_, P::Message, P::Theme, P::Renderer>, >, - window_manager: &mut WindowManager<A, C>, + window_manager: &mut WindowManager<P, C>, ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, ) where - A: Application, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, + P: Program, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: DefaultStyle, { use crate::runtime::clipboard; use crate::runtime::system; @@ -1047,12 +1082,6 @@ fn run_action<A, C>( window::Action::Close(id) => { let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); - - if window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); - } } window::Action::Drag(id) => { if let Some(window) = window_manager.get_mut(id) { @@ -1266,19 +1295,24 @@ fn run_action<A, C>( let _ = channel.send(Ok(())); } + Action::Exit => { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + } } } /// Build the user interface for every window. -pub fn build_user_interfaces<'a, A: Application, C>( - application: &'a A, +pub fn build_user_interfaces<'a, P: Program, C>( + application: &'a P, debug: &mut Debug, - window_manager: &mut WindowManager<A, C>, + window_manager: &mut WindowManager<P, C>, mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, -) -> FxHashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> +) -> FxHashMap<window::Id, UserInterface<'a, P::Message, P::Theme, P::Renderer>> where - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, { cached_user_interfaces .drain() @@ -1300,7 +1334,7 @@ where .collect() } -/// Returns true if the provided event should cause an [`Application`] to +/// Returns true if the provided event should cause a [`Program`] to /// exit. pub fn user_force_quit( event: &winit::event::WindowEvent, diff --git a/winit/src/multi_window/state.rs b/winit/src/program/state.rs similarity index 90% rename from winit/src/multi_window/state.rs rename to winit/src/program/state.rs index dfd8e696..a7fa2788 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/program/state.rs @@ -2,16 +2,16 @@ use crate::conversion; use crate::core::{mouse, window}; use crate::core::{Color, Size}; use crate::graphics::Viewport; -use crate::multi_window::{self, Application}; +use crate::program::{self, Program}; use std::fmt::{Debug, Formatter}; use winit::event::{Touch, WindowEvent}; use winit::window::Window; -/// The state of a multi-windowed [`Application`]. -pub struct State<A: Application> +/// The state of a multi-windowed [`Program`]. +pub struct State<P: Program> where - A::Theme: multi_window::DefaultStyle, + P::Theme: program::DefaultStyle, { title: String, scale_factor: f64, @@ -19,13 +19,13 @@ where viewport_version: u64, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: multi_window::Appearance, + theme: P::Theme, + appearance: program::Appearance, } -impl<A: Application> Debug for State<A> +impl<P: Program> Debug for State<P> where - A::Theme: multi_window::DefaultStyle, + P::Theme: program::DefaultStyle, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("multi_window::State") @@ -39,13 +39,13 @@ where } } -impl<A: Application> State<A> +impl<P: Program> State<P> where - A::Theme: multi_window::DefaultStyle, + P::Theme: program::DefaultStyle, { - /// Creates a new [`State`] for the provided [`Application`]'s `window`. + /// Creates a new [`State`] for the provided [`Program`]'s `window`. pub fn new( - application: &A, + application: &P, window_id: window::Id, window: &Window, ) -> Self { @@ -121,7 +121,7 @@ where } /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &A::Theme { + pub fn theme(&self) -> &P::Theme { &self.theme } @@ -195,14 +195,14 @@ where } } - /// Synchronizes the [`State`] with its [`Application`] and its respective + /// Synchronizes the [`State`] with its [`Program`] and its respective /// window. /// - /// Normally, an [`Application`] should be synchronized with its [`State`] + /// Normally, a [`Program`] should be synchronized with its [`State`] /// and window after calling [`State::update`]. pub fn synchronize( &mut self, - application: &A, + application: &P, window_id: window::Id, window: &Window, ) { diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/program/window_manager.rs similarity index 73% rename from winit/src/multi_window/window_manager.rs rename to winit/src/program/window_manager.rs index 57a7dc7e..fcbf79f6 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -2,28 +2,28 @@ use crate::core::mouse; use crate::core::window::Id; use crate::core::{Point, Size}; use crate::graphics::Compositor; -use crate::multi_window::{Application, DefaultStyle, State}; +use crate::program::{DefaultStyle, Program, State}; use std::collections::BTreeMap; use std::sync::Arc; use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] -pub struct WindowManager<A, C> +pub struct WindowManager<P, C> where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, { aliases: BTreeMap<winit::window::WindowId, Id>, - entries: BTreeMap<Id, Window<A, C>>, + entries: BTreeMap<Id, Window<P, C>>, } -impl<A, C> WindowManager<A, C> +impl<P, C> WindowManager<P, C> where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, { pub fn new() -> Self { Self { @@ -36,10 +36,10 @@ where &mut self, id: Id, window: Arc<winit::window::Window>, - application: &A, + application: &P, compositor: &mut C, exit_on_close_request: bool, - ) -> &mut Window<A, C> { + ) -> &mut Window<P, C> { let state = State::new(application, id, &window); let viewport_version = state.viewport_version(); let physical_size = state.physical_size(); @@ -76,18 +76,18 @@ where pub fn iter_mut( &mut self, - ) -> impl Iterator<Item = (Id, &mut Window<A, C>)> { + ) -> impl Iterator<Item = (Id, &mut Window<P, C>)> { self.entries.iter_mut().map(|(k, v)| (*k, v)) } - pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<A, C>> { + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> { self.entries.get_mut(&id) } pub fn get_mut_alias( &mut self, id: winit::window::WindowId, - ) -> Option<(Id, &mut Window<A, C>)> { + ) -> Option<(Id, &mut Window<P, C>)> { let id = self.aliases.get(&id).copied()?; Some((id, self.get_mut(id)?)) @@ -97,7 +97,7 @@ where self.entries.values().last()?.raw.current_monitor() } - pub fn remove(&mut self, id: Id) -> Option<Window<A, C>> { + pub fn remove(&mut self, id: Id) -> Option<Window<P, C>> { let window = self.entries.remove(&id)?; let _ = self.aliases.remove(&window.raw.id()); @@ -105,11 +105,11 @@ where } } -impl<A, C> Default for WindowManager<A, C> +impl<P, C> Default for WindowManager<P, C> where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, { fn default() -> Self { Self::new() @@ -117,26 +117,26 @@ where } #[allow(missing_debug_implementations)] -pub struct Window<A, C> +pub struct Window<P, C> where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, { pub raw: Arc<winit::window::Window>, - pub state: State<A>, + pub state: State<P>, pub viewport_version: u64, pub exit_on_close_request: bool, pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, - pub renderer: A::Renderer, + pub renderer: P::Renderer, } -impl<A, C> Window<A, C> +impl<P, C> Window<P, C> where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, { pub fn position(&self) -> Option<Point> { self.raw diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 0ab61375..d8ad8b3f 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -78,11 +78,22 @@ impl<T: 'static> Proxy<T> { /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! pub fn send(&mut self, value: T) + where + T: std::fmt::Debug, + { + self.send_action(Action::Output(value)); + } + + /// Sends an action to the event loop. + /// + /// Note: This skips the backpressure mechanism with an unbounded + /// channel. Use sparingly! + pub fn send_action(&mut self, action: Action<T>) where T: std::fmt::Debug, { self.raw - .send_event(Action::Output(value)) + .send_event(action) .expect("Send message to event loop"); } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 2e541128..78368a04 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,25 +1,15 @@ //! Configure your application. -use crate::core::window; - use std::borrow::Cow; /// The settings of an application. #[derive(Debug, Clone, Default)] -pub struct Settings<Flags> { +pub struct Settings { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or /// communicate with it through the windowing system. pub id: Option<String>, - /// The [`window::Settings`]. - pub window: window::Settings, - - /// The data needed to initialize an [`Application`]. - /// - /// [`Application`]: crate::Application - pub flags: Flags, - /// The fonts to load on boot. pub fonts: Vec<Cow<'static, [u8]>>, } From 5f259434497fcc3b39d377181c05fe78f566d475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 19 Jun 2024 17:39:17 +0200 Subject: [PATCH 059/657] Fix WebAssembly compilation Rendering seems to still not work, however. --- winit/src/program.rs | 112 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 105 insertions(+), 7 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 28cd8e52..8e3563f7 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -8,12 +8,12 @@ use crate::conversion; use crate::core; use crate::core::mouse; use crate::core::renderer; +use crate::core::time::Instant; use crate::core::widget::operation; use crate::core::window; use crate::core::{Color, Element, Point, Size, Theme}; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; -use crate::futures::futures::executor; use crate::futures::futures::task; use crate::futures::futures::{Future, StreamExt}; use crate::futures::subscription::{self, Subscription}; @@ -30,7 +30,6 @@ use window_manager::WindowManager; use rustc_hash::FxHashMap; use std::mem::ManuallyDrop; use std::sync::Arc; -use std::time::Instant; /// An interactive, native, cross-platform, multi-windowed application. /// @@ -231,6 +230,11 @@ where sender: mpsc::UnboundedSender<Event<Message>>, receiver: mpsc::UnboundedReceiver<Control>, error: Option<Error>, + + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc<std::cell::RefCell<bool>>, + #[cfg(target_arch = "wasm32")] + queued_events: Vec<Event<Message>>, } struct BootConfig<C> { @@ -239,7 +243,7 @@ where graphics_settings: graphics::Settings, } - let mut runner = Runner { + let runner = Runner { instance, context, id: settings.id, @@ -251,13 +255,18 @@ where sender: event_sender, receiver: control_receiver, error: None, + + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), + #[cfg(target_arch = "wasm32")] + queued_events: Vec::new(), }; impl<Message, F, C> winit::application::ApplicationHandler<Message> for Runner<Message, F, C> where F: Future<Output = ()>, - C: Compositor, + C: Compositor + 'static, { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let Some(BootConfig { @@ -298,10 +307,24 @@ where Ok::<_, graphics::Error>(()) }; - if let Err(error) = executor::block_on(finish_boot) { + #[cfg(not(target_arch = "wasm32"))] + if let Err(error) = + crate::futures::futures::executor::block_on(finish_boot) + { self.error = Some(Error::GraphicsCreationFailed(error)); event_loop.exit(); } + + #[cfg(target_arch = "wasm32")] + { + let is_booted = self.is_booted.clone(); + + wasm_bindgen_futures::spawn_local(async move { + finish_boot.await.expect("Finish boot!"); + + *is_booted.borrow_mut() = true; + }); + } } fn new_events( @@ -391,6 +414,19 @@ where event_loop: &winit::event_loop::ActiveEventLoop, event: Event<Message>, ) { + #[cfg(target_arch = "wasm32")] + if !*self.is_booted.borrow() { + self.queued_events.push(event); + return; + } else if !self.queued_events.is_empty() { + let queued_events = std::mem::take(&mut self.queued_events); + + // This won't infinitely recurse, since we `mem::take` + for event in queued_events { + self.process_event(event_loop, event); + } + } + if event_loop.exiting() { return; } @@ -429,6 +465,10 @@ where let exit_on_close_request = settings.exit_on_close_request; + #[cfg(target_arch = "wasm32")] + let target = + settings.platform_specific.target.clone(); + let window = event_loop .create_window( conversion::window_attributes( @@ -442,6 +482,52 @@ where ) .expect("Create window"); + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window + .canvas() + .expect("Get window canvas"); + + let _ = canvas.set_attribute( + "style", + "display: block; width: 100%; height: 100%", + ); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!( + "#{target}" + )) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1( + &canvas, + ) + .expect(&format!( + "Could not replace #{}", + node.id() + )); + } + None => { + let _ = body + .append_child(&canvas) + .expect( + "Append canvas to HTML body", + ); + } + }; + } + self.process_event( event_loop, Event::WindowCreated { @@ -468,9 +554,21 @@ where } } - let _ = event_loop.run_app(&mut runner); + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); - Ok(()) + runner.error.map(Err).unwrap_or(Ok(())) + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); + + Ok(()) + } } struct Boot<C> { From 65c8e08b440f012e4a26cb55ff5554cd36fde614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 19 Jun 2024 19:03:07 +0200 Subject: [PATCH 060/657] Fix initialization race conditions in WebAssembly WebGL is still broken, but oh well... Time to move on. --- winit/src/program.rs | 100 ++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 58 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 8e3563f7..62f8b6af 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -200,20 +200,22 @@ where Runtime::new(executor, proxy.clone()) }; - let (application, task) = runtime.enter(|| P::new(flags)); + let (program, task) = runtime.enter(|| P::new(flags)); if let Some(stream) = task.into_stream() { runtime.run(stream); } + runtime.track(program.subscription().map(Action::Output).into_recipes()); + let (boot_sender, boot_receiver) = oneshot::channel(); let (event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, control_receiver) = mpsc::unbounded(); let instance = Box::pin(run_instance::<P, C>( - application, + program, runtime, - proxy, + proxy.clone(), debug, boot_receiver, event_receiver, @@ -226,18 +228,19 @@ where instance: std::pin::Pin<Box<F>>, context: task::Context<'static>, id: Option<String>, - boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<Event<Message>>, + boot: Option<BootConfig<Message, C>>, + sender: mpsc::UnboundedSender<Event<Action<Message>>>, receiver: mpsc::UnboundedReceiver<Control>, error: Option<Error>, #[cfg(target_arch = "wasm32")] is_booted: std::rc::Rc<std::cell::RefCell<bool>>, #[cfg(target_arch = "wasm32")] - queued_events: Vec<Event<Message>>, + queued_events: Vec<Event<Action<Message>>>, } - struct BootConfig<C> { + struct BootConfig<Message: 'static, C> { + proxy: Proxy<Message>, sender: oneshot::Sender<Boot<C>>, window_settings: Option<window::Settings>, graphics_settings: graphics::Settings, @@ -248,6 +251,7 @@ where context, id: settings.id, boot: Some(BootConfig { + proxy, sender: boot_sender, window_settings, graphics_settings, @@ -262,14 +266,16 @@ where queued_events: Vec::new(), }; - impl<Message, F, C> winit::application::ApplicationHandler<Message> + impl<Message, F, C> winit::application::ApplicationHandler<Action<Message>> for Runner<Message, F, C> where + Message: std::fmt::Debug, F: Future<Output = ()>, C: Compositor + 'static, { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let Some(BootConfig { + mut proxy, sender, window_settings, graphics_settings, @@ -299,11 +305,23 @@ where .send(Boot { compositor, clipboard, - window_settings, + is_daemon: window_settings.is_none(), }) .ok() .expect("Send boot event"); + if let Some(window_settings) = window_settings { + let (sender, _receiver) = oneshot::channel(); + + proxy.send_action(Action::Window( + runtime::window::Action::Open( + window::Id::unique(), + window_settings, + sender, + ), + )); + } + Ok::<_, graphics::Error>(()) }; @@ -383,12 +401,12 @@ where fn user_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - message: Message, + action: Action<Message>, ) { self.process_event( event_loop, Event::EventLoopAwakened(winit::event::Event::UserEvent( - message, + action, )), ); } @@ -412,7 +430,7 @@ where fn process_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - event: Event<Message>, + event: Event<Action<Message>>, ) { #[cfg(target_arch = "wasm32")] if !*self.is_booted.borrow() { @@ -574,7 +592,7 @@ where struct Boot<C> { compositor: C, clipboard: Clipboard, - window_settings: Option<window::Settings>, + is_daemon: bool, } enum Event<Message: 'static> { @@ -616,7 +634,7 @@ async fn run_instance<P, C>( let Boot { mut compositor, mut clipboard, - window_settings, + is_daemon, } = boot.try_recv().ok().flatten().expect("Receive boot"); let mut window_manager = WindowManager::new(); @@ -626,29 +644,7 @@ async fn run_instance<P, C>( let mut actions = 0; let mut ui_caches = FxHashMap::default(); - let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( - &program, - &mut debug, - &mut window_manager, - FxHashMap::from_iter([( - window::Id::MAIN, - user_interface::Cache::default(), - )]), - )); - - runtime.track(program.subscription().map(Action::Output).into_recipes()); - - let is_daemon = window_settings.is_none(); - - if let Some(window_settings) = window_settings { - let (sender, _receiver) = oneshot::channel(); - - proxy.send_action(Action::Window(runtime::window::Action::Open( - window::Id::unique(), - window_settings, - sender, - ))); - } + let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); debug.startup_finished(); @@ -697,8 +693,6 @@ async fn run_instance<P, C>( | event::StartCause::ResumeTimeReached { .. }, ) => { for (_id, window) in window_manager.iter_mut() { - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw window.raw.request_redraw(); } } @@ -878,9 +872,6 @@ async fn run_instance<P, C>( ) { Ok(()) => { debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. } Err(error) => match error { // This is an unrecoverable error. @@ -1024,7 +1015,6 @@ async fn run_instance<P, C>( debug.event_processing_finished(); - // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { let cached_interfaces: FxHashMap< window::Id, @@ -1034,7 +1024,6 @@ async fn run_instance<P, C>( .map(|(id, ui)| (id, ui.into_cache())) .collect(); - // Update application update( &mut program, &mut runtime, @@ -1042,8 +1031,6 @@ async fn run_instance<P, C>( &mut messages, ); - // we must synchronize all window states with application state after an - // application update since we don't know what changed for (id, window) in window_manager.iter_mut() { window.state.synchronize( &program, @@ -1051,12 +1038,9 @@ async fn run_instance<P, C>( &window.raw, ); - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw window.raw.request_redraw(); } - // rebuild UIs with the synchronized states user_interfaces = ManuallyDrop::new(build_user_interfaces( &program, @@ -1082,7 +1066,7 @@ async fn run_instance<P, C>( /// Builds a window's [`UserInterface`] for the [`Program`]. fn build_user_interface<'a, P: Program>( - application: &'a P, + program: &'a P, cache: user_interface::Cache, renderer: &mut P::Renderer, size: Size, @@ -1093,7 +1077,7 @@ where P::Theme: DefaultStyle, { debug.view_started(); - let view = application.view(id); + let view = program.view(id); debug.view_finished(); debug.layout_started(); @@ -1104,7 +1088,7 @@ where } fn update<P: Program, E: Executor>( - application: &mut P, + program: &mut P, runtime: &mut Runtime<E, Proxy<P::Message>, Action<P::Message>>, debug: &mut Debug, messages: &mut Vec<P::Message>, @@ -1115,7 +1099,7 @@ fn update<P: Program, E: Executor>( debug.log_message(&message); debug.update_started(); - let task = runtime.enter(|| application.update(message)); + let task = runtime.enter(|| program.update(message)); debug.update_finished(); if let Some(stream) = task.into_stream() { @@ -1123,13 +1107,13 @@ fn update<P: Program, E: Executor>( } } - let subscription = application.subscription(); + let subscription = program.subscription(); runtime.track(subscription.map(Action::Output).into_recipes()); } fn run_action<P, C>( action: Action<P::Message>, - application: &P, + program: &P, compositor: &mut C, messages: &mut Vec<P::Message>, clipboard: &mut Clipboard, @@ -1170,7 +1154,7 @@ fn run_action<P, C>( .start_send(Control::CreateWindow { id, settings, - title: application.title(id), + title: program.title(id), monitor, }) .expect("Send control action"); @@ -1403,7 +1387,7 @@ fn run_action<P, C>( /// Build the user interface for every window. pub fn build_user_interfaces<'a, P: Program, C>( - application: &'a P, + program: &'a P, debug: &mut Debug, window_manager: &mut WindowManager<P, C>, mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, @@ -1420,7 +1404,7 @@ where Some(( id, build_user_interface( - application, + program, cache, &mut window.renderer, window.state.logical_size(), From bdd30f7ab8c2280c368b86cb12baec0578669d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 20 Jun 2024 01:11:21 +0200 Subject: [PATCH 061/657] Introduce `and_then` methods for fallible `Task`s --- runtime/src/task.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 740360ac..b8a83d6d 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -242,6 +242,39 @@ impl<T> Task<T> { } } +impl<T> Task<Option<T>> { + /// Executes a new [`Task`] after this one, only when it produces `Some` value. + /// + /// The value is provided to the closure to create the subsequent [`Task`]. + pub fn and_then<A>( + self, + f: impl Fn(T) -> Task<A> + MaybeSend + 'static, + ) -> Task<A> + where + T: MaybeSend + 'static, + A: MaybeSend + 'static, + { + self.then(move |option| option.map_or_else(Task::none, &f)) + } +} + +impl<T, E> Task<Result<T, E>> { + /// Executes a new [`Task`] after this one, only when it succeeds with an `Ok` value. + /// + /// The success value is provided to the closure to create the subsequent [`Task`]. + pub fn and_then<A>( + self, + f: impl Fn(T) -> Task<A> + MaybeSend + 'static, + ) -> Task<A> + where + T: MaybeSend + 'static, + E: MaybeSend + 'static, + A: MaybeSend + 'static, + { + self.then(move |option| option.map_or_else(|_| Task::none(), &f)) + } +} + impl<T> From<()> for Task<T> where T: MaybeSend + 'static, From 92e08c8f07511cc212cbce545fb7739ef1a4bf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 20 Jun 2024 01:13:09 +0200 Subject: [PATCH 062/657] Add `get_latest` and `get_oldest` tasks in `window` --- runtime/src/window.rs | 76 ++++++++++++++++++++++++++----------------- winit/src/program.rs | 24 ++++++++++---- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 956a20e1..b04e5d59 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -26,6 +26,12 @@ pub enum Action { /// Close the window and exits the application. Close(Id), + /// Gets the [`Id`] of the oldest window. + GetOldest(oneshot::Sender<Option<Id>>), + + /// Gets the [`Id`] of the latest window. + GetLatest(oneshot::Sender<Option<Id>>), + /// Move the window with the left mouse button until the button is /// released. /// @@ -36,26 +42,26 @@ pub enum Action { /// Resize the window to the given logical dimensions. Resize(Id, Size), - /// Fetch the current logical dimensions of the window. - FetchSize(Id, oneshot::Sender<Size>), + /// Get the current logical dimensions of the window. + GetSize(Id, oneshot::Sender<Size>), - /// Fetch if the current window is maximized or not. - FetchMaximized(Id, oneshot::Sender<bool>), + /// Get if the current window is maximized or not. + GetMaximized(Id, oneshot::Sender<bool>), /// Set the window to maximized or back Maximize(Id, bool), - /// Fetch if the current window is minimized or not. + /// Get if the current window is minimized or not. /// /// ## Platform-specific /// - **Wayland:** Always `None`. - FetchMinimized(Id, oneshot::Sender<Option<bool>>), + GetMinimized(Id, oneshot::Sender<Option<bool>>), /// Set the window to minimized or back Minimize(Id, bool), - /// Fetch the current logical coordinates of the window. - FetchPosition(Id, oneshot::Sender<Option<Point>>), + /// Get the current logical coordinates of the window. + GetPosition(Id, oneshot::Sender<Option<Point>>), /// Move the window to the given logical coordinates. /// @@ -65,8 +71,8 @@ pub enum Action { /// Change the [`Mode`] of the window. ChangeMode(Id, Mode), - /// Fetch the current [`Mode`] of the window. - FetchMode(Id, oneshot::Sender<Mode>), + /// Get the current [`Mode`] of the window. + GetMode(Id, oneshot::Sender<Mode>), /// Toggle the window to maximized or back ToggleMaximize(Id), @@ -114,8 +120,8 @@ pub enum Action { /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. ShowSystemMenu(Id), - /// Fetch the raw identifier unique to the window. - FetchRawId(Id, oneshot::Sender<u64>), + /// Get the raw identifier unique to the window. + GetRawId(Id, oneshot::Sender<u64>), /// Change the window [`Icon`]. /// @@ -214,6 +220,16 @@ pub fn close<T>(id: Id) -> Task<T> { Task::effect(crate::Action::Window(Action::Close(id))) } +/// Gets the window [`Id`] of the oldest window. +pub fn get_oldest() -> Task<Option<Id>> { + Task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) +} + +/// Gets the window [`Id`] of the latest window. +pub fn get_latest() -> Task<Option<Id>> { + Task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) +} + /// Begins dragging the window while the left mouse button is held. pub fn drag<T>(id: Id) -> Task<T> { Task::effect(crate::Action::Window(Action::Drag(id))) @@ -224,17 +240,17 @@ pub fn resize<T>(id: Id, new_size: Size) -> Task<T> { Task::effect(crate::Action::Window(Action::Resize(id, new_size))) } -/// Fetches the window's size in logical dimensions. -pub fn fetch_size(id: Id) -> Task<Size> { +/// Get the window's size in logical dimensions. +pub fn get_size(id: Id) -> Task<Size> { Task::oneshot(move |channel| { - crate::Action::Window(Action::FetchSize(id, channel)) + crate::Action::Window(Action::GetSize(id, channel)) }) } -/// Fetches if the window is maximized. -pub fn fetch_maximized(id: Id) -> Task<bool> { +/// Gets the maximized state of the window with the given [`Id`]. +pub fn get_maximized(id: Id) -> Task<bool> { Task::oneshot(move |channel| { - crate::Action::Window(Action::FetchMaximized(id, channel)) + crate::Action::Window(Action::GetMaximized(id, channel)) }) } @@ -243,10 +259,10 @@ pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> { Task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } -/// Fetches if the window is minimized. -pub fn fetch_minimized(id: Id) -> Task<Option<bool>> { +/// Gets the minimized state of the window with the given [`Id`]. +pub fn get_minimized(id: Id) -> Task<Option<bool>> { Task::oneshot(move |channel| { - crate::Action::Window(Action::FetchMinimized(id, channel)) + crate::Action::Window(Action::GetMinimized(id, channel)) }) } @@ -255,10 +271,10 @@ pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> { Task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } -/// Fetches the current window position in logical coordinates. -pub fn fetch_position(id: Id) -> Task<Option<Point>> { +/// Gets the position in logical coordinates of the window with the given [`Id`]. +pub fn get_position(id: Id) -> Task<Option<Point>> { Task::oneshot(move |channel| { - crate::Action::Window(Action::FetchPosition(id, channel)) + crate::Action::Window(Action::GetPosition(id, channel)) }) } @@ -272,10 +288,10 @@ pub fn change_mode<T>(id: Id, mode: Mode) -> Task<T> { Task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) } -/// Fetches the current [`Mode`] of the window. -pub fn fetch_mode(id: Id) -> Task<Mode> { +/// Gets the current [`Mode`] of the window. +pub fn get_mode(id: Id) -> Task<Mode> { Task::oneshot(move |channel| { - crate::Action::Window(Action::FetchMode(id, channel)) + crate::Action::Window(Action::GetMode(id, channel)) }) } @@ -327,11 +343,11 @@ pub fn show_system_menu<T>(id: Id) -> Task<T> { Task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } -/// Fetches an identifier unique to the window, provided by the underlying windowing system. This is +/// Gets an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. -pub fn fetch_raw_id<Message>(id: Id) -> Task<u64> { +pub fn get_raw_id<Message>(id: Id) -> Task<u64> { Task::oneshot(|channel| { - crate::Action::Window(Action::FetchRawId(id, channel)) + crate::Action::Window(Action::GetRawId(id, channel)) }) } diff --git a/winit/src/program.rs b/winit/src/program.rs index 62f8b6af..2e7945ac 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1165,6 +1165,18 @@ fn run_action<P, C>( let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); } + window::Action::GetOldest(channel) => { + let id = + window_manager.iter_mut().next().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::GetLatest(channel) => { + let id = + window_manager.iter_mut().last().map(|(id, _window)| id); + + let _ = channel.send(id); + } window::Action::Drag(id) => { if let Some(window) = window_manager.get_mut(id) { let _ = window.raw.drag_window(); @@ -1180,7 +1192,7 @@ fn run_action<P, C>( ); } } - window::Action::FetchSize(id, channel) => { + window::Action::GetSize(id, channel) => { if let Some(window) = window_manager.get_mut(id) { let size = window .raw @@ -1190,7 +1202,7 @@ fn run_action<P, C>( let _ = channel.send(Size::new(size.width, size.height)); } } - window::Action::FetchMaximized(id, channel) => { + window::Action::GetMaximized(id, channel) => { if let Some(window) = window_manager.get_mut(id) { let _ = channel.send(window.raw.is_maximized()); } @@ -1200,7 +1212,7 @@ fn run_action<P, C>( window.raw.set_maximized(maximized); } } - window::Action::FetchMinimized(id, channel) => { + window::Action::GetMinimized(id, channel) => { if let Some(window) = window_manager.get_mut(id) { let _ = channel.send(window.raw.is_minimized()); } @@ -1210,7 +1222,7 @@ fn run_action<P, C>( window.raw.set_minimized(minimized); } } - window::Action::FetchPosition(id, channel) => { + window::Action::GetPosition(id, channel) => { if let Some(window) = window_manager.get_mut(id) { let position = window .raw @@ -1250,7 +1262,7 @@ fn run_action<P, C>( window.raw.set_window_icon(conversion::icon(icon)); } } - window::Action::FetchMode(id, channel) => { + window::Action::GetMode(id, channel) => { if let Some(window) = window_manager.get_mut(id) { let mode = if window.raw.is_visible().unwrap_or(true) { conversion::mode(window.raw.fullscreen()) @@ -1304,7 +1316,7 @@ fn run_action<P, C>( } } } - window::Action::FetchRawId(id, channel) => { + window::Action::GetRawId(id, channel) => { if let Some(window) = window_manager.get_mut(id) { let _ = channel.send(window.raw.id().into()); } From c5f4bebeda8d6ef10efade7933a5ee58f06b62d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 20 Jun 2024 01:13:42 +0200 Subject: [PATCH 063/657] Remove `window::Id::MAIN` constant --- core/src/window/id.rs | 3 --- examples/events/src/main.rs | 4 ++-- examples/exit/src/main.rs | 2 +- examples/multi_window/src/main.rs | 2 +- examples/screenshot/src/main.rs | 3 ++- examples/todos/src/main.rs | 7 ++++--- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 20474c8f..31ea92f3 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -11,9 +11,6 @@ pub struct Id(u64); static COUNT: AtomicU64 = AtomicU64::new(1); impl Id { - /// The reserved window [`Id`] for the first window in an Iced application. - pub const MAIN: Self = Id(0); - /// Creates a new unique window [`Id`]. pub fn unique() -> Id { Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 4f0f07b0..2cd3c5d8 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -38,7 +38,7 @@ impl Events { } Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { - window::close(window::Id::MAIN) + window::get_latest().and_then(window::close) } else { Task::none() } @@ -48,7 +48,7 @@ impl Events { Task::none() } - Message::Exit => window::close(window::Id::MAIN), + Message::Exit => window::get_latest().and_then(window::close), } } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index b998016e..1f108df2 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -20,7 +20,7 @@ enum Message { impl Exit { fn update(&mut self, message: Message) -> Task<Message> { match message { - Message::Confirm => window::close(window::Id::MAIN), + Message::Confirm => window::get_latest().and_then(window::close), Message::Exit => { self.show_confirm = true; diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index dfb816cf..98e753ab 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -57,7 +57,7 @@ impl Example { return Task::none(); }; - window::fetch_position(*last_window) + window::get_position(*last_window) .then(|last_position| { let position = last_position.map_or( window::Position::Default, diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 1ea53e8f..acde8367 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -47,7 +47,8 @@ impl Example { fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Screenshot => { - return iced::window::screenshot(window::Id::MAIN) + return window::get_latest() + .and_then(window::screenshot) .map(Message::Screenshotted); } Message::Screenshotted(screenshot) => { diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index a834c946..6ed50d31 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -149,9 +149,10 @@ impl Todos { widget::focus_next() } } - Message::ToggleFullscreen(mode) => { - window::change_mode(window::Id::MAIN, mode) - } + Message::ToggleFullscreen(mode) => window::get_latest() + .and_then(move |window| { + window::change_mode(window, mode) + }), Message::Loaded(_) => Command::none(), }; From 3334cf670b09ee2ef0d06c9005677e024050f121 Mon Sep 17 00:00:00 2001 From: ryankopf <git@ryankopf.com> Date: Thu, 20 Jun 2024 00:40:37 -0500 Subject: [PATCH 064/657] feat: Add methods for window settings in Application This commit adds new methods to the `Application` struct for setting various window settings such as resizable, decorations, position, and level. These methods allow for more customization and control over the appearance and behavior of the application window. --- src/application.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/application.rs b/src/application.rs index edca6e79..7b292e23 100644 --- a/src/application.rs +++ b/src/application.rs @@ -256,6 +256,50 @@ impl<P: Program> Application<P> { } } + /// Sets the [`window::Settings::resizable`] of the [`Application`]. + pub fn resizable(self, resizable: bool) -> Self { + Self { + window: window::Settings { + resizable, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::decorations`] of the [`Application`]. + pub fn decorations(self, decorations: bool) -> Self { + Self { + window: window::Settings { + decorations, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::position`] of the [`Application`]. + pub fn position(self, position: window::Position) -> Self { + Self { + window: window::Settings { + position, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::level`] of the [`Application`]. + pub fn level(self, level: window::Level) -> Self { + Self { + window: window::Settings { + level, + ..self.window + }, + ..self + } + } + /// Sets the [`Title`] of the [`Application`]. pub(crate) fn title( self, From 0785b334e79d8f973c96b86608823f54afdf93c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 20 Jun 2024 18:35:10 +0200 Subject: [PATCH 065/657] Add `window` method to `Application` --- src/application.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/application.rs b/src/application.rs index 7b292e23..5d16b40f 100644 --- a/src/application.rs +++ b/src/application.rs @@ -212,6 +212,13 @@ impl<P: Program> Application<P> { self } + /// Sets the [`window::Settings`] of the [`Application`]. + /// + /// Overwrites any previous [`window::Settings`]. + pub fn window(self, window: window::Settings) -> Self { + Self { window, ..self } + } + /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Application`]. pub fn centered(self) -> Self { Self { @@ -288,7 +295,7 @@ impl<P: Program> Application<P> { ..self } } - + /// Sets the [`window::Settings::level`] of the [`Application`]. pub fn level(self, level: window::Level) -> Self { Self { From cbeda38f0d49f184ff26b7f77a4246b33ea3bd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 20 Jun 2024 18:50:03 +0200 Subject: [PATCH 066/657] Inline documentation for `application` and `daemon` functions --- src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 957c7ecc..bc3fe6ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -306,8 +306,8 @@ pub mod widget { mod runtime {} } -pub use application::{application, Application}; -pub use daemon::{daemon, Daemon}; +pub use application::Application; +pub use daemon::Daemon; pub use error::Error; pub use event::Event; pub use executor::Executor; @@ -316,6 +316,11 @@ pub use renderer::Renderer; pub use settings::Settings; pub use subscription::Subscription; +#[doc(inline)] +pub use application::application; +#[doc(inline)] +pub use daemon::daemon; + /// A generic widget. /// /// This is an alias of an `iced_native` element with a default `Renderer`. From 50dd2a6cc03fdc184b7a9fb0f7a659952a742a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 20 Jun 2024 22:28:28 +0200 Subject: [PATCH 067/657] Fix `application` sometimes exiting at startup --- winit/src/program.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/winit/src/program.rs b/winit/src/program.rs index 2e7945ac..9bb3fa21 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -305,6 +305,7 @@ where .send(Boot { compositor, clipboard, + window: window.id(), is_daemon: window_settings.is_none(), }) .ok() @@ -592,6 +593,7 @@ where struct Boot<C> { compositor: C, clipboard: Clipboard, + window: winit::window::WindowId, is_daemon: bool, } @@ -634,6 +636,7 @@ async fn run_instance<P, C>( let Boot { mut compositor, mut clipboard, + window: boot_window, is_daemon, } = boot.try_recv().ok().flatten().expect("Receive boot"); @@ -905,6 +908,7 @@ async fn run_instance<P, C>( window_event, winit::event::WindowEvent::Destroyed ) + && window_id != boot_window && window_manager.is_empty() { break 'main; From a7224a782751f2927b8bcce7ade26d3557563ae5 Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz <vlad@vladh.net> Date: Fri, 21 Jun 2024 11:55:42 +0100 Subject: [PATCH 068/657] Implement Copy on Fill and Stroke --- graphics/src/geometry/fill.rs | 2 +- graphics/src/geometry/stroke.rs | 2 +- graphics/src/geometry/style.rs | 2 +- graphics/src/gradient.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 670fbc12..b79a2582 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -7,7 +7,7 @@ use crate::core::Color; use crate::gradient::{self, Gradient}; /// The style used to fill geometry. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Fill { /// The color or gradient of the fill. /// diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index aff49ab3..b8f4515e 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -6,7 +6,7 @@ pub use crate::geometry::Style; use iced_core::Color; /// The style of a stroke. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Stroke<'a> { /// The color or gradient of the stroke. /// diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index a0f4b08a..de77eccc 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -2,7 +2,7 @@ use crate::core::Color; use crate::geometry::Gradient; /// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Style { /// A solid [`Color`]. Solid(Color), diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs index 603f1b4a..54261721 100644 --- a/graphics/src/gradient.rs +++ b/graphics/src/gradient.rs @@ -9,7 +9,7 @@ use bytemuck::{Pod, Zeroable}; use half::f16; use std::cmp::Ordering; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] /// A fill which linearly interpolates colors along a direction. /// /// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`]. From e8b1e5a112e7f54689947137932aa18dd46f567a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 21 Jun 2024 15:38:51 +0200 Subject: [PATCH 069/657] Fix fonts not being loaded at startup --- winit/src/program.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 9bb3fa21..d55aedf1 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -28,6 +28,7 @@ use crate::{Clipboard, Error, Proxy, Settings}; use window_manager::WindowManager; use rustc_hash::FxHashMap; +use std::borrow::Cow; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -242,6 +243,7 @@ where struct BootConfig<Message: 'static, C> { proxy: Proxy<Message>, sender: oneshot::Sender<Boot<C>>, + fonts: Vec<Cow<'static, [u8]>>, window_settings: Option<window::Settings>, graphics_settings: graphics::Settings, } @@ -253,6 +255,7 @@ where boot: Some(BootConfig { proxy, sender: boot_sender, + fonts: settings.fonts, window_settings, graphics_settings, }), @@ -277,6 +280,7 @@ where let Some(BootConfig { mut proxy, sender, + fonts, window_settings, graphics_settings, }) = self.boot.take() @@ -298,9 +302,13 @@ where let clipboard = Clipboard::connect(&window); let finish_boot = async move { - let compositor = + let mut compositor = C::new(graphics_settings, window.clone()).await?; + for font in fonts { + compositor.load_font(font); + } + sender .send(Boot { compositor, From b9eb86199afe0f2d936eb4ab90af5b2a2c32a87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 21 Jun 2024 21:28:57 +0200 Subject: [PATCH 070/657] Remove unnecessary `Send` bound in `runtime::Action` This may fix compilation errors in older versions of Rust. --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b4a5e819..7e46593a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -60,7 +60,7 @@ pub enum Action<T> { }, /// Run a widget operation. - Widget(Box<dyn widget::Operation<()> + Send>), + Widget(Box<dyn widget::Operation<()>>), /// Run a clipboard action. Clipboard(clipboard::Action), From 2ac80f8e9ccbcb8538ca6e613488c3b0eb9ef124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 28 Jun 2024 23:24:29 +0200 Subject: [PATCH 071/657] Fix `window::open_events` subscribing to closed events Fixes #2481. --- runtime/src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index b04e5d59..3e53dd55 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -175,7 +175,7 @@ pub fn events() -> Subscription<(Id, Event)> { /// Subscribes to all [`Event::Closed`] occurrences in the running application. pub fn open_events() -> Subscription<Id> { event::listen_with(|event, _status, id| { - if let crate::core::Event::Window(Event::Closed) = event { + if let crate::core::Event::Window(Event::Opened { .. }) = event { Some(id) } else { None From 5c2185f123f58cc54364dabba900d3a10052bc88 Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz <vlad@vladh.net> Date: Sat, 29 Jun 2024 17:19:48 +0100 Subject: [PATCH 072/657] wgpu: fix "radii" typo --- wgpu/src/shader/quad.wgsl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index a367d5e6..b213c8cf 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -22,14 +22,14 @@ fn rounded_box_sdf(to_center: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 { return length(max(abs(to_center) - size + vec2<f32>(radius, radius), vec2<f32>(0.0, 0.0))) - radius; } -// Based on the fragment position and the center of the quad, select one of the 4 radi. +// Based on the fragment position and the center of the quad, select one of the 4 radii. // Order matches CSS border radius attribute: -// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left -fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { - var rx = radi.x; - var ry = radi.y; - rx = select(radi.x, radi.y, position.x > center.x); - ry = select(radi.w, radi.z, position.x > center.x); +// radii.x = top-left, radii.y = top-right, radii.z = bottom-right, radii.w = bottom-left +fn select_border_radius(radii: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { + var rx = radii.x; + var ry = radii.y; + rx = select(radii.x, radii.y, position.x > center.x); + ry = select(radii.w, radii.z, position.x > center.x); rx = select(rx, ry, position.y > center.y); return rx; } From be59ec0ffce20cf789b18499e37e3e3cb5505b5b Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz <vlad@vladh.net> Date: Sat, 29 Jun 2024 21:39:12 +0100 Subject: [PATCH 073/657] doc: clarify Quad border alignment --- core/src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/renderer.rs b/core/src/renderer.rs index a2785ae8..6684517f 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -69,7 +69,7 @@ pub struct Quad { /// The bounds of the [`Quad`]. pub bounds: Rectangle, - /// The [`Border`] of the [`Quad`]. + /// The [`Border`] of the [`Quad`]. The border is drawn on the inside of the [`Quad`]. pub border: Border, /// The [`Shadow`] of the [`Quad`]. From 10ef48c98af1c29e2fab5ea636915d311db339d5 Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz <vlad@vladh.net> Date: Sun, 30 Jun 2024 20:46:04 +0100 Subject: [PATCH 074/657] doc: fix "Reconciles" typo --- core/src/widget.rs | 2 +- core/src/widget/tree.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/widget.rs b/core/src/widget.rs index 0d12deba..b17215bb 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -96,7 +96,7 @@ where Vec::new() } - /// Reconciliates the [`Widget`] with the provided [`Tree`]. + /// Reconciles the [`Widget`] with the provided [`Tree`]. fn diff(&self, _tree: &mut Tree) {} /// Applies an [`Operation`] to the [`Widget`]. diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 6b1a1309..2600cfc6 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -46,7 +46,7 @@ impl Tree { } } - /// Reconciliates the current tree with the provided [`Widget`]. + /// Reconciles the current tree with the provided [`Widget`]. /// /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the /// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called). @@ -81,7 +81,7 @@ impl Tree { ); } - /// Reconciliates the children of the tree with the provided list of widgets using custom + /// Reconciles the children of the tree with the provided list of widgets using custom /// logic both for diffing and creating new widget state. pub fn diff_children_custom<T>( &mut self, @@ -107,7 +107,7 @@ impl Tree { } } -/// Reconciliates the `current_children` with the provided list of widgets using +/// Reconciles the `current_children` with the provided list of widgets using /// custom logic both for diffing and creating new widget state. /// /// The algorithm will try to minimize the impact of diffing by querying the From 39c21e9fc20d031db4c16f481ba0a4910b621841 Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz <vlad@vladh.net> Date: Tue, 2 Jul 2024 14:06:55 +0100 Subject: [PATCH 075/657] doc: remove extraneous comment --- widget/src/scrollable.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c3d08223..bd612fa6 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,5 +1,4 @@ //! Navigate an endless amount of content with a scrollbar. -// use crate::container; use crate::container; use crate::core::event::{self, Event}; use crate::core::keyboard; From 2b19471d1cfe4cf034b026aa6620b1685a5ab772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 2 Jul 2024 19:01:04 +0200 Subject: [PATCH 076/657] Simplify `subscription::channel` example --- futures/src/subscription.rs | 43 ++++++++++++------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 316fc44d..5ec39582 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -369,44 +369,29 @@ where /// // ... /// } /// -/// enum State { -/// Starting, -/// Ready(mpsc::Receiver<Input>), -/// } -/// /// fn some_worker() -> Subscription<Event> { /// struct SomeWorker; /// /// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move { -/// let mut state = State::Starting; +/// // Create channel +/// let (sender, mut receiver) = mpsc::channel(100); +/// +/// // Send the sender back to the application +/// output.send(Event::Ready(sender)).await; /// /// loop { -/// match &mut state { -/// State::Starting => { -/// // Create channel -/// let (sender, receiver) = mpsc::channel(100); +/// use iced_futures::futures::StreamExt; /// -/// // Send the sender back to the application -/// output.send(Event::Ready(sender)).await; +/// // Read next input sent from `Application` +/// let input = receiver.select_next_some().await; /// -/// // We are ready to receive messages -/// state = State::Ready(receiver); -/// } -/// State::Ready(receiver) => { -/// use iced_futures::futures::StreamExt; +/// match input { +/// Input::DoSomeWork => { +/// // Do some async work... /// -/// // Read next input sent from `Application` -/// let input = receiver.select_next_some().await; -/// -/// match input { -/// Input::DoSomeWork => { -/// // Do some async work... -/// -/// // Finally, we can optionally produce a message to tell the -/// // `Application` the work is done -/// output.send(Event::WorkFinished).await; -/// } -/// } +/// // Finally, we can optionally produce a message to tell the +/// // `Application` the work is done +/// output.send(Event::WorkFinished).await; /// } /// } /// } From 88611d7653e2a22c82c41f8d1a4732c2af60adcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 5 Jul 2024 01:13:28 +0200 Subject: [PATCH 077/657] Hide internal `Task` constructors --- runtime/src/clipboard.rs | 10 +-- runtime/src/font.rs | 5 +- runtime/src/lib.rs | 5 +- runtime/src/task.rs | 167 ++++++++++++++++++++------------------- runtime/src/window.rs | 52 ++++++------ widget/src/container.rs | 4 +- widget/src/helpers.rs | 7 +- widget/src/scrollable.rs | 7 +- widget/src/text_input.rs | 13 +-- winit/src/program.rs | 4 +- winit/src/system.rs | 2 +- 11 files changed, 140 insertions(+), 136 deletions(-) diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index 19950d01..a02cc011 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,7 +1,7 @@ //! Access the clipboard. use crate::core::clipboard::Kind; use crate::futures::futures::channel::oneshot; -use crate::Task; +use crate::task::{self, Task}; /// A clipboard action to be performed by some [`Task`]. /// @@ -27,7 +27,7 @@ pub enum Action { /// Read the current contents of the clipboard. pub fn read() -> Task<Option<String>> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Clipboard(Action::Read { target: Kind::Standard, channel, @@ -37,7 +37,7 @@ pub fn read() -> Task<Option<String>> { /// Read the current contents of the primary clipboard. pub fn read_primary() -> Task<Option<String>> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Clipboard(Action::Read { target: Kind::Primary, channel, @@ -47,7 +47,7 @@ pub fn read_primary() -> Task<Option<String>> { /// Write the given contents to the clipboard. pub fn write<T>(contents: String) -> Task<T> { - Task::effect(crate::Action::Clipboard(Action::Write { + task::effect(crate::Action::Clipboard(Action::Write { target: Kind::Standard, contents, })) @@ -55,7 +55,7 @@ pub fn write<T>(contents: String) -> Task<T> { /// Write the given contents to the primary clipboard. pub fn write_primary<Message>(contents: String) -> Task<Message> { - Task::effect(crate::Action::Clipboard(Action::Write { + task::effect(crate::Action::Clipboard(Action::Write { target: Kind::Primary, contents, })) diff --git a/runtime/src/font.rs b/runtime/src/font.rs index d54eb6a8..75fdfc11 100644 --- a/runtime/src/font.rs +++ b/runtime/src/font.rs @@ -1,5 +1,6 @@ //! Load and use fonts. -use crate::{Action, Task}; +use crate::task::{self, Task}; +use crate::Action; use std::borrow::Cow; /// An error while loading a font. @@ -8,7 +9,7 @@ pub enum Error {} /// Load a font from its bytes. pub fn load(bytes: impl Into<Cow<'static, [u8]>>) -> Task<Result<(), Error>> { - Task::oneshot(|channel| Action::LoadFont { + task::oneshot(|channel| Action::LoadFont { bytes: bytes.into(), channel, }) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7e46593a..f27657d1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -15,14 +15,13 @@ pub mod keyboard; pub mod overlay; pub mod program; pub mod system; +pub mod task; pub mod user_interface; pub mod window; #[cfg(feature = "multi-window")] pub mod multi_window; -mod task; - // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] @@ -127,5 +126,5 @@ where /// This will normally close any application windows and /// terminate the runtime loop. pub fn exit<T>() -> Task<T> { - Task::effect(Action::Exit) + task::effect(Action::Exit) } diff --git a/runtime/src/task.rs b/runtime/src/task.rs index b8a83d6d..e037c403 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -1,3 +1,4 @@ +//! Create runtime tasks. use crate::core::widget; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; @@ -29,24 +30,6 @@ impl<T> Task<T> { Self::future(future::ready(value)) } - /// Creates a new [`Task`] that runs the given [`Future`] and produces - /// its output. - pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self::stream(stream::once(future)) - } - - /// Creates a new [`Task`] that runs the given [`Stream`] and produces - /// each of its items. - pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self(Some(boxed_stream(stream.map(Action::Output)))) - } - /// Creates a [`Task`] that runs the given [`Future`] to completion and maps its /// output with the given closure. pub fn perform<A>( @@ -72,6 +55,24 @@ impl<T> Task<T> { Self::stream(stream.map(f)) } + /// Creates a new [`Task`] that runs the given [`Future`] and produces + /// its output. + pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self::stream(stream::once(future)) + } + + /// Creates a new [`Task`] that runs the given [`Stream`] and produces + /// each of its items. + pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream.map(Action::Output)))) + } + /// Combines the given tasks and produces a single [`Task`] that will run all of them /// in parallel. pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self @@ -83,66 +84,6 @@ impl<T> Task<T> { )))) } - /// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces - /// its output. - pub fn widget(operation: impl widget::Operation<T> + 'static) -> Task<T> - where - T: Send + 'static, - { - Self::channel(move |sender| { - let operation = - widget::operation::map(Box::new(operation), move |value| { - let _ = sender.clone().try_send(value); - }); - - Action::Widget(Box::new(operation)) - }) - } - - /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and - /// produces the value fed to the [`oneshot::Sender`]. - pub fn oneshot(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T> - where - T: MaybeSend + 'static, - { - let (sender, receiver) = oneshot::channel(); - - let action = f(sender); - - Self(Some(boxed_stream( - stream::once(async move { action }).chain( - receiver.into_stream().filter_map(|result| async move { - Some(Action::Output(result.ok()?)) - }), - ), - ))) - } - - /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and - /// produces the values fed to the [`mpsc::Sender`]. - pub fn channel(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T> - where - T: MaybeSend + 'static, - { - let (sender, receiver) = mpsc::channel(1); - - let action = f(sender); - - Self(Some(boxed_stream( - stream::once(async move { action }) - .chain(receiver.map(|result| Action::Output(result))), - ))) - } - - /// Creates a new [`Task`] that executes the given [`Action`] and produces no output. - pub fn effect(action: impl Into<Action<Never>>) -> Self { - let action = action.into(); - - Self(Some(boxed_stream(stream::once(async move { - action.output().expect_err("no output") - })))) - } - /// Maps the output of a [`Task`] with the given closure. pub fn map<O>( self, @@ -235,11 +176,6 @@ impl<T> Task<T> { ))), } } - - /// Returns the underlying [`Stream`] of the [`Task`]. - pub fn into_stream(self) -> Option<BoxStream<Action<T>>> { - self.0 - } } impl<T> Task<Option<T>> { @@ -283,3 +219,68 @@ where Self::none() } } + +/// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces +/// its output. +pub fn widget<T>(operation: impl widget::Operation<T> + 'static) -> Task<T> +where + T: Send + 'static, +{ + channel(move |sender| { + let operation = + widget::operation::map(Box::new(operation), move |value| { + let _ = sender.clone().try_send(value); + }); + + Action::Widget(Box::new(operation)) + }) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the value fed to the [`oneshot::Sender`]. +pub fn oneshot<T>(f: impl FnOnce(oneshot::Sender<T>) -> Action<T>) -> Task<T> +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = oneshot::channel(); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }).chain( + receiver.into_stream().filter_map(|result| async move { + Some(Action::Output(result.ok()?)) + }), + ), + ))) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the values fed to the [`mpsc::Sender`]. +pub fn channel<T>(f: impl FnOnce(mpsc::Sender<T>) -> Action<T>) -> Task<T> +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }) + .chain(receiver.map(|result| Action::Output(result))), + ))) +} + +/// Creates a new [`Task`] that executes the given [`Action`] and produces no output. +pub fn effect<T>(action: impl Into<Action<Never>>) -> Task<T> { + let action = action.into(); + + Task(Some(boxed_stream(stream::once(async move { + action.output().expect_err("no output") + })))) +} + +/// Returns the underlying [`Stream`] of the [`Task`]. +pub fn into_stream<T>(task: Task<T>) -> Option<BoxStream<Action<T>>> { + task.0 +} diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 3e53dd55..815827d1 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -11,7 +11,7 @@ use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::futures::channel::oneshot; use crate::futures::Subscription; -use crate::Task; +use crate::task::{self, Task}; pub use raw_window_handle; @@ -210,99 +210,99 @@ pub fn close_requests() -> Subscription<Id> { pub fn open(settings: Settings) -> Task<Id> { let id = Id::unique(); - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Window(Action::Open(id, settings, channel)) }) } /// Closes the window with `id`. pub fn close<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::Close(id))) + task::effect(crate::Action::Window(Action::Close(id))) } /// Gets the window [`Id`] of the oldest window. pub fn get_oldest() -> Task<Option<Id>> { - Task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) + task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) } /// Gets the window [`Id`] of the latest window. pub fn get_latest() -> Task<Option<Id>> { - Task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) + task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) } /// Begins dragging the window while the left mouse button is held. pub fn drag<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::Drag(id))) + task::effect(crate::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. pub fn resize<T>(id: Id, new_size: Size) -> Task<T> { - Task::effect(crate::Action::Window(Action::Resize(id, new_size))) + task::effect(crate::Action::Window(Action::Resize(id, new_size))) } /// Get the window's size in logical dimensions. pub fn get_size(id: Id) -> Task<Size> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetSize(id, channel)) }) } /// Gets the maximized state of the window with the given [`Id`]. pub fn get_maximized(id: Id) -> Task<bool> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMaximized(id, channel)) }) } /// Maximizes the window. pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> { - Task::effect(crate::Action::Window(Action::Maximize(id, maximized))) + task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } /// Gets the minimized state of the window with the given [`Id`]. pub fn get_minimized(id: Id) -> Task<Option<bool>> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMinimized(id, channel)) }) } /// Minimizes the window. pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> { - Task::effect(crate::Action::Window(Action::Minimize(id, minimized))) + task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } /// Gets the position in logical coordinates of the window with the given [`Id`]. pub fn get_position(id: Id) -> Task<Option<Point>> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetPosition(id, channel)) }) } /// Moves the window to the given logical coordinates. pub fn move_to<T>(id: Id, position: Point) -> Task<T> { - Task::effect(crate::Action::Window(Action::Move(id, position))) + task::effect(crate::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. pub fn change_mode<T>(id: Id, mode: Mode) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) + task::effect(crate::Action::Window(Action::ChangeMode(id, mode))) } /// Gets the current [`Mode`] of the window. pub fn get_mode(id: Id) -> Task<Mode> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::GetMode(id, channel)) }) } /// Toggles the window to maximized or back. pub fn toggle_maximize<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ToggleMaximize(id))) + task::effect(crate::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. pub fn toggle_decorations<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ToggleDecorations(id))) + task::effect(crate::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -315,7 +315,7 @@ pub fn request_user_attention<T>( id: Id, user_attention: Option<UserAttention>, ) -> Task<T> { - Task::effect(crate::Action::Window(Action::RequestUserAttention( + task::effect(crate::Action::Window(Action::RequestUserAttention( id, user_attention, ))) @@ -328,32 +328,32 @@ pub fn request_user_attention<T>( /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. pub fn gain_focus<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::GainFocus(id))) + task::effect(crate::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. pub fn change_level<T>(id: Id, level: Level) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) + task::effect(crate::Action::Window(Action::ChangeLevel(id, level))) } /// Show the [system menu] at cursor position. /// /// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu pub fn show_system_menu<T>(id: Id) -> Task<T> { - Task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) + task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } /// Gets an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. pub fn get_raw_id<Message>(id: Id) -> Task<u64> { - Task::oneshot(|channel| { + task::oneshot(|channel| { crate::Action::Window(Action::GetRawId(id, channel)) }) } /// Changes the [`Icon`] of the window. pub fn change_icon<T>(id: Id, icon: Icon) -> Task<T> { - Task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) + task::effect(crate::Action::Window(Action::ChangeIcon(id, icon))) } /// Runs the given callback with the native window handle for the window with the given id. @@ -366,7 +366,7 @@ pub fn run_with_handle<T>( where T: Send + 'static, { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::RunWithHandle( id, Box::new(move |handle| { @@ -378,7 +378,7 @@ where /// Captures a [`Screenshot`] from the window. pub fn screenshot(id: Id) -> Task<Screenshot> { - Task::oneshot(move |channel| { + task::oneshot(move |channel| { crate::Action::Window(Action::Screenshot(id, channel)) }) } diff --git a/widget/src/container.rs b/widget/src/container.rs index e917471f..08d5cb17 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -13,7 +13,7 @@ use crate::core::{ Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Task; +use crate::runtime::task::{self, Task}; /// An element decorating some content. /// @@ -538,7 +538,7 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> { } } - Task::widget(VisibleBounds { + task::widget(VisibleBounds { target: id.into(), depth: 0, scrollables: Vec::new(), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 62343a55..9056d191 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -12,7 +12,8 @@ use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; use crate::rule::{self, Rule}; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -930,12 +931,12 @@ where /// Focuses the previous focusable widget. pub fn focus_previous<T>() -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus_previous())) + task::effect(Action::widget(operation::focusable::focus_previous())) } /// Focuses the next focusable widget. pub fn focus_next<T>() -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus_next())) + task::effect(Action::widget(operation::focusable::focus_next())) } /// A container intercepting mouse events. diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c3d08223..3ff1f8a1 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -15,7 +15,8 @@ use crate::core::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; @@ -955,13 +956,13 @@ impl From<Id> for widget::Id { /// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided `percentage` along the x & y axis. pub fn snap_to<T>(id: Id, offset: RelativeOffset) -> Task<T> { - Task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) + task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// to the provided [`AbsoluteOffset`] along the x & y axis. pub fn scroll_to<T>(id: Id, offset: AbsoluteOffset) -> Task<T> { - Task::effect(Action::widget(operation::scrollable::scroll_to( + task::effect(Action::widget(operation::scrollable::scroll_to( id.0, offset, ))) } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 4e89236b..ba2fbc13 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -30,7 +30,8 @@ use crate::core::{ Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::{Action, Task}; +use crate::runtime::task::{self, Task}; +use crate::runtime::Action; /// A field that can be filled with text. /// @@ -1142,13 +1143,13 @@ impl From<Id> for widget::Id { /// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. pub fn focus<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::focusable::focus(id.0))) + task::effect(Action::widget(operation::focusable::focus(id.0))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to_end( + task::effect(Action::widget(operation::text_input::move_cursor_to_end( id.0, ))) } @@ -1156,7 +1157,7 @@ pub fn move_cursor_to_end<T>(id: Id) -> Task<T> { /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to_front( + task::effect(Action::widget(operation::text_input::move_cursor_to_front( id.0, ))) } @@ -1164,14 +1165,14 @@ pub fn move_cursor_to_front<T>(id: Id) -> Task<T> { /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. pub fn move_cursor_to<T>(id: Id, position: usize) -> Task<T> { - Task::effect(Action::widget(operation::text_input::move_cursor_to( + task::effect(Action::widget(operation::text_input::move_cursor_to( id.0, position, ))) } /// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. pub fn select_all<T>(id: Id) -> Task<T> { - Task::effect(Action::widget(operation::text_input::select_all(id.0))) + task::effect(Action::widget(operation::text_input::select_all(id.0))) } /// The state of a [`TextInput`]. diff --git a/winit/src/program.rs b/winit/src/program.rs index d55aedf1..e1693196 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -203,7 +203,7 @@ where let (program, task) = runtime.enter(|| P::new(flags)); - if let Some(stream) = task.into_stream() { + if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); } @@ -1114,7 +1114,7 @@ fn update<P: Program, E: Executor>( let task = runtime.enter(|| program.update(message)); debug.update_finished(); - if let Some(stream) = task.into_stream() { + if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); } } diff --git a/winit/src/system.rs b/winit/src/system.rs index 7997f311..361135be 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -5,7 +5,7 @@ use crate::runtime::{self, Task}; /// Query for available system information. pub fn fetch_information() -> Task<Information> { - Task::oneshot(|channel| { + runtime::task::oneshot(|channel| { runtime::Action::System(Action::QueryInformation(channel)) }) } From 8bc49cd88653309f5abe8a38d5a4af36fcfea933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 5 Jul 2024 02:15:13 +0200 Subject: [PATCH 078/657] Hide `Subscription` internals .. and introduce `stream::channel` helper --- examples/download_progress/src/download.rs | 14 +- examples/websocket/src/echo.rs | 100 +++--- examples/websocket/src/main.rs | 2 +- futures/src/backend/native/async_std.rs | 2 +- futures/src/backend/native/smol.rs | 2 +- futures/src/backend/native/tokio.rs | 2 +- futures/src/lib.rs | 1 + futures/src/stream.rs | 26 ++ futures/src/subscription.rs | 349 +++++++++++---------- src/lib.rs | 10 +- winit/src/program.rs | 6 +- 11 files changed, 268 insertions(+), 246 deletions(-) create mode 100644 futures/src/stream.rs diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index d6cc1e24..bdf57290 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -1,4 +1,5 @@ -use iced::subscription; +use iced::futures; +use iced::Subscription; use std::hash::Hash; @@ -7,9 +8,14 @@ pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>( id: I, url: T, ) -> iced::Subscription<(I, Progress)> { - subscription::unfold(id, State::Ready(url.to_string()), move |state| { - download(id, state) - }) + Subscription::run_with_id( + id, + futures::stream::unfold(State::Ready(url.to_string()), move |state| { + use iced::futures::FutureExt; + + download(id, state).map(Some) + }), + ) } async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) { diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index cd32cb66..14652936 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -1,87 +1,79 @@ pub mod server; use iced::futures; -use iced::subscription::{self, Subscription}; +use iced::stream; use iced::widget::text; use futures::channel::mpsc; use futures::sink::SinkExt; -use futures::stream::StreamExt; +use futures::stream::{Stream, StreamExt}; use async_tungstenite::tungstenite; use std::fmt; -pub fn connect() -> Subscription<Event> { - struct Connect; +pub fn connect() -> impl Stream<Item = Event> { + stream::channel(100, |mut output| async move { + let mut state = State::Disconnected; - subscription::channel( - std::any::TypeId::of::<Connect>(), - 100, - |mut output| async move { - let mut state = State::Disconnected; + loop { + match &mut state { + State::Disconnected => { + const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - loop { - match &mut state { - State::Disconnected => { - const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; - - match async_tungstenite::tokio::connect_async( - ECHO_SERVER, - ) + match async_tungstenite::tokio::connect_async(ECHO_SERVER) .await - { - Ok((websocket, _)) => { - let (sender, receiver) = mpsc::channel(100); + { + Ok((websocket, _)) => { + let (sender, receiver) = mpsc::channel(100); - let _ = output - .send(Event::Connected(Connection(sender))) - .await; - - state = State::Connected(websocket, receiver); - } - Err(_) => { - tokio::time::sleep( - tokio::time::Duration::from_secs(1), - ) + let _ = output + .send(Event::Connected(Connection(sender))) .await; - let _ = output.send(Event::Disconnected).await; - } + state = State::Connected(websocket, receiver); + } + Err(_) => { + tokio::time::sleep( + tokio::time::Duration::from_secs(1), + ) + .await; + + let _ = output.send(Event::Disconnected).await; } } - State::Connected(websocket, input) => { - let mut fused_websocket = websocket.by_ref().fuse(); + } + State::Connected(websocket, input) => { + let mut fused_websocket = websocket.by_ref().fuse(); - futures::select! { - received = fused_websocket.select_next_some() => { - match received { - Ok(tungstenite::Message::Text(message)) => { - let _ = output.send(Event::MessageReceived(Message::User(message))).await; - } - Err(_) => { - let _ = output.send(Event::Disconnected).await; - - state = State::Disconnected; - } - Ok(_) => continue, + futures::select! { + received = fused_websocket.select_next_some() => { + match received { + Ok(tungstenite::Message::Text(message)) => { + let _ = output.send(Event::MessageReceived(Message::User(message))).await; } - } - - message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; - - if result.is_err() { + Err(_) => { let _ = output.send(Event::Disconnected).await; state = State::Disconnected; } + Ok(_) => continue, + } + } + + message = input.select_next_some() => { + let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; + + if result.is_err() { + let _ = output.send(Event::Disconnected).await; + + state = State::Disconnected; } } } } } - }, - ) + } + }) } #[derive(Debug)] diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 8422ce16..95a14fd9 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -83,7 +83,7 @@ impl WebSocket { } fn subscription(&self) -> Subscription<Message> { - echo::connect().map(Message::Echo) + Subscription::run(echo::connect).map(Message::Echo) } fn view(&self) -> Element<Message> { diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs index b7da5e90..86714f45 100644 --- a/futures/src/backend/native/async_std.rs +++ b/futures/src/backend/native/async_std.rs @@ -27,7 +27,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs index aaf1518c..8d448e7f 100644 --- a/futures/src/backend/native/smol.rs +++ b/futures/src/backend/native/smol.rs @@ -26,7 +26,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs index df91d798..9dc3593d 100644 --- a/futures/src/backend/native/tokio.rs +++ b/futures/src/backend/native/tokio.rs @@ -31,7 +31,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] diff --git a/futures/src/lib.rs b/futures/src/lib.rs index a874a618..31738823 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -15,6 +15,7 @@ pub mod backend; pub mod event; pub mod executor; pub mod keyboard; +pub mod stream; pub mod subscription; pub use executor::Executor; diff --git a/futures/src/stream.rs b/futures/src/stream.rs new file mode 100644 index 00000000..2ec505f1 --- /dev/null +++ b/futures/src/stream.rs @@ -0,0 +1,26 @@ +//! Create asynchronous streams of data. +use futures::channel::mpsc; +use futures::never::Never; +use futures::stream::{self, Stream, StreamExt}; + +use std::future::Future; + +/// Creates a new [`Stream`] that produces the items sent from a [`Future`] +/// to the [`mpsc::Sender`] provided to the closure. +/// +/// This is a more ergonomic [`stream::unfold`], which allows you to go +/// from the "world of futures" to the "world of streams" by simply looping +/// and publishing to an async channel from inside a [`Future`]. +pub fn channel<T, F>( + size: usize, + f: impl FnOnce(mpsc::Sender<T>) -> F, +) -> impl Stream<Item = T> +where + F: Future<Output = Never>, +{ + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).map(|_| unreachable!()); + + stream::select(receiver, runner) +} diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 5ec39582..1a0d454d 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -5,11 +5,9 @@ pub use tracker::Tracker; use crate::core::event; use crate::core::window; -use crate::futures::{Future, Stream}; +use crate::futures::Stream; use crate::{BoxStream, MaybeSend}; -use futures::channel::mpsc; -use futures::never::Never; use std::any::TypeId; use std::hash::Hash; @@ -61,20 +59,66 @@ pub type Hasher = rustc_hash::FxHasher; /// A request to listen to external events. /// -/// Besides performing async actions on demand with `Command`, most +/// Besides performing async actions on demand with `Task`, most /// applications also need to listen to external events passively. /// -/// A [`Subscription`] is normally provided to some runtime, like a `Command`, +/// A [`Subscription`] is normally provided to some runtime, like a `Task`, /// and it will generate events as long as the user keeps requesting it. /// /// For instance, you can use a [`Subscription`] to listen to a `WebSocket` /// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// # The Lifetime of a [`Subscription`] +/// Much like a [`Future`] or a [`Stream`], a [`Subscription`] does not produce any effects +/// on its own. For a [`Subscription`] to run, it must be returned to the iced runtime—normally +/// in the `subscription` function of an `application` or a `daemon`. +/// +/// When a [`Subscription`] is provided to the runtime for the first time, the runtime will +/// start running it asynchronously. Running a [`Subscription`] consists in building its underlying +/// [`Stream`] and executing it in an async runtime. +/// +/// Therefore, you can think of a [`Subscription`] as a "stream builder". It simply represents a way +/// to build a certain [`Stream`] together with some way to _identify_ it. +/// +/// Identification is important because when a specific [`Subscription`] stops being returned to the +/// iced runtime, the runtime will kill its associated [`Stream`]. The runtime uses the identity of a +/// [`Subscription`] to keep track of it. +/// +/// This way, iced allows you to declaratively __subscribe__ to particular streams of data temporarily +/// and whenever necessary. +/// +/// ``` +/// # mod iced { +/// # pub mod time { +/// # pub use iced_futures::backend::default::time::every; +/// # pub use std::time::{Duration, Instant}; +/// # } +/// # +/// # pub use iced_futures::Subscription; +/// # } +/// use iced::time::{self, Duration, Instant}; +/// use iced::Subscription; +/// +/// struct State { +/// timer_enabled: bool, +/// } +/// +/// fn subscription(state: &State) -> Subscription<Instant> { +/// if state.timer_enabled { +/// time::every(Duration::from_secs(1)) +/// } else { +/// Subscription::none() +/// } +/// } +/// ``` +/// +/// [`Future`]: std::future::Future #[must_use = "`Subscription` must be returned to runtime to take effect"] -pub struct Subscription<Message> { - recipes: Vec<Box<dyn Recipe<Output = Message>>>, +pub struct Subscription<T> { + recipes: Vec<Box<dyn Recipe<Output = T>>>, } -impl<Message> Subscription<Message> { +impl<T> Subscription<T> { /// Returns an empty [`Subscription`] that will not produce any output. pub fn none() -> Self { Self { @@ -82,19 +126,102 @@ impl<Message> Subscription<Message> { } } - /// Creates a [`Subscription`] from a [`Recipe`] describing it. - pub fn from_recipe( - recipe: impl Recipe<Output = Message> + 'static, - ) -> Self { - Self { - recipes: vec![Box::new(recipe)], - } + /// Returns a [`Subscription`] that will call the given function to create and + /// asynchronously run the given [`Stream`]. + /// + /// # Creating an asynchronous worker with bidirectional communication + /// You can leverage this helper to create a [`Subscription`] that spawns + /// an asynchronous worker in the background and establish a channel of + /// communication with an `iced` application. + /// + /// You can achieve this by creating an `mpsc` channel inside the closure + /// and returning the `Sender` as a `Message` for the `Application`: + /// + /// ``` + /// use iced_futures::subscription::{self, Subscription}; + /// use iced_futures::stream; + /// use iced_futures::futures::channel::mpsc; + /// use iced_futures::futures::sink::SinkExt; + /// use iced_futures::futures::Stream; + /// + /// pub enum Event { + /// Ready(mpsc::Sender<Input>), + /// WorkFinished, + /// // ... + /// } + /// + /// enum Input { + /// DoSomeWork, + /// // ... + /// } + /// + /// fn some_worker() -> impl Stream<Item = Event> { + /// stream::channel(100, |mut output| async move { + /// // Create channel + /// let (sender, mut receiver) = mpsc::channel(100); + /// + /// // Send the sender back to the application + /// output.send(Event::Ready(sender)).await; + /// + /// loop { + /// use iced_futures::futures::StreamExt; + /// + /// // Read next input sent from `Application` + /// let input = receiver.select_next_some().await; + /// + /// match input { + /// Input::DoSomeWork => { + /// // Do some async work... + /// + /// // Finally, we can optionally produce a message to tell the + /// // `Application` the work is done + /// output.send(Event::WorkFinished).await; + /// } + /// } + /// } + /// }) + /// } + /// + /// fn subscription() -> Subscription<Event> { + /// Subscription::run(some_worker) + /// } + /// ``` + /// + /// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` + /// connection open. + /// + /// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket + pub fn run<S>(builder: fn() -> S) -> Self + where + S: Stream<Item = T> + MaybeSend + 'static, + T: 'static, + { + from_recipe(Runner { + id: builder, + spawn: move |_| builder(), + }) + } + + /// Returns a [`Subscription`] that will create and asynchronously run the + /// given [`Stream`]. + /// + /// The `id` will be used to uniquely identify the [`Subscription`]. + pub fn run_with_id<I, S>(id: I, stream: S) -> Subscription<T> + where + I: Hash + 'static, + S: Stream<Item = T> + MaybeSend + 'static, + T: 'static, + { + from_recipe(Runner { + id, + spawn: move |_| stream, + }) } /// Batches all the provided subscriptions and returns the resulting /// [`Subscription`]. pub fn batch( - subscriptions: impl IntoIterator<Item = Subscription<Message>>, + subscriptions: impl IntoIterator<Item = Subscription<T>>, ) -> Self { Self { recipes: subscriptions @@ -104,18 +231,13 @@ impl<Message> Subscription<Message> { } } - /// Returns the different recipes of the [`Subscription`]. - pub fn into_recipes(self) -> Vec<Box<dyn Recipe<Output = Message>>> { - self.recipes - } - /// Adds a value to the [`Subscription`] context. /// /// The value will be part of the identity of a [`Subscription`]. - pub fn with<T>(mut self, value: T) -> Subscription<(T, Message)> + pub fn with<A>(mut self, value: A) -> Subscription<(A, T)> where - Message: 'static, - T: std::hash::Hash + Clone + Send + Sync + 'static, + T: 'static, + A: std::hash::Hash + Clone + Send + Sync + 'static, { Subscription { recipes: self @@ -123,7 +245,7 @@ impl<Message> Subscription<Message> { .drain(..) .map(|recipe| { Box::new(With::new(recipe, value.clone())) - as Box<dyn Recipe<Output = (T, Message)>> + as Box<dyn Recipe<Output = (A, T)>> }) .collect(), } @@ -136,8 +258,8 @@ impl<Message> Subscription<Message> { /// will panic in debug mode otherwise. pub fn map<F, A>(mut self, f: F) -> Subscription<A> where - Message: 'static, - F: Fn(Message) -> A + MaybeSend + Clone + 'static, + T: 'static, + F: Fn(T) -> A + MaybeSend + Clone + 'static, A: 'static, { debug_assert!( @@ -159,7 +281,23 @@ impl<Message> Subscription<Message> { } } -impl<Message> std::fmt::Debug for Subscription<Message> { +/// Creates a [`Subscription`] from a [`Recipe`] describing it. +pub fn from_recipe<T>( + recipe: impl Recipe<Output = T> + 'static, +) -> Subscription<T> { + Subscription { + recipes: vec![Box::new(recipe)], + } +} + +/// Returns the different recipes of the [`Subscription`]. +pub fn into_recipes<T>( + subscription: Subscription<T>, +) -> Vec<Box<dyn Recipe<Output = T>>> { + subscription.recipes +} + +impl<T> std::fmt::Debug for Subscription<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Subscription").finish() } @@ -273,65 +411,13 @@ where } } -/// Returns a [`Subscription`] that will call the given function to create and -/// asynchronously run the given [`Stream`]. -pub fn run<S, Message>(builder: fn() -> S) -> Subscription<Message> -where - S: Stream<Item = Message> + MaybeSend + 'static, - Message: 'static, -{ - Subscription::from_recipe(Runner { - id: builder, - spawn: move |_| builder(), - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run the -/// given [`Stream`]. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn run_with_id<I, S, Message>(id: I, stream: S) -> Subscription<Message> +pub(crate) fn filter_map<I, F, T>(id: I, f: F) -> Subscription<T> where I: Hash + 'static, - S: Stream<Item = Message> + MaybeSend + 'static, - Message: 'static, + F: Fn(Event) -> Option<T> + MaybeSend + 'static, + T: 'static + MaybeSend, { - Subscription::from_recipe(Runner { - id, - spawn: move |_| stream, - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run a -/// [`Stream`] that will call the provided closure to produce every `Message`. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn unfold<I, T, Fut, Message>( - id: I, - initial: T, - mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, -) -> Subscription<Message> -where - I: Hash + 'static, - T: MaybeSend + 'static, - Fut: Future<Output = (Message, T)> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::future::FutureExt; - - run_with_id( - id, - futures::stream::unfold(initial, move |state| f(state).map(Some)), - ) -} - -pub(crate) fn filter_map<I, F, Message>(id: I, f: F) -> Subscription<Message> -where - I: Hash + 'static, - F: Fn(Event) -> Option<Message> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - Subscription::from_recipe(Runner { + from_recipe(Runner { id, spawn: |events| { use futures::future; @@ -342,107 +428,22 @@ where }) } -/// Creates a [`Subscription`] that publishes the events sent from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -/// -/// # Creating an asynchronous worker with bidirectional communication -/// You can leverage this helper to create a [`Subscription`] that spawns -/// an asynchronous worker in the background and establish a channel of -/// communication with an `iced` application. -/// -/// You can achieve this by creating an `mpsc` channel inside the closure -/// and returning the `Sender` as a `Message` for the `Application`: -/// -/// ``` -/// use iced_futures::subscription::{self, Subscription}; -/// use iced_futures::futures::channel::mpsc; -/// use iced_futures::futures::sink::SinkExt; -/// -/// pub enum Event { -/// Ready(mpsc::Sender<Input>), -/// WorkFinished, -/// // ... -/// } -/// -/// enum Input { -/// DoSomeWork, -/// // ... -/// } -/// -/// fn some_worker() -> Subscription<Event> { -/// struct SomeWorker; -/// -/// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move { -/// // Create channel -/// let (sender, mut receiver) = mpsc::channel(100); -/// -/// // Send the sender back to the application -/// output.send(Event::Ready(sender)).await; -/// -/// loop { -/// use iced_futures::futures::StreamExt; -/// -/// // Read next input sent from `Application` -/// let input = receiver.select_next_some().await; -/// -/// match input { -/// Input::DoSomeWork => { -/// // Do some async work... -/// -/// // Finally, we can optionally produce a message to tell the -/// // `Application` the work is done -/// output.send(Event::WorkFinished).await; -/// } -/// } -/// } -/// }) -/// } -/// ``` -/// -/// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` -/// connection open. -/// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket -pub fn channel<I, Fut, Message>( - id: I, - size: usize, - f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, -) -> Subscription<Message> -where - I: Hash + 'static, - Fut: Future<Output = Never> + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::stream::{self, StreamExt}; - - Subscription::from_recipe(Runner { - id, - spawn: move |_| { - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).map(|_| unreachable!()); - - stream::select(receiver, runner) - }, - }) -} - -struct Runner<I, F, S, Message> +struct Runner<I, F, S, T> where F: FnOnce(EventStream) -> S, - S: Stream<Item = Message>, + S: Stream<Item = T>, { id: I, spawn: F, } -impl<I, S, F, Message> Recipe for Runner<I, F, S, Message> +impl<I, F, S, T> Recipe for Runner<I, F, S, T> where I: Hash + 'static, F: FnOnce(EventStream) -> S, - S: Stream<Item = Message> + MaybeSend + 'static, + S: Stream<Item = T> + MaybeSend + 'static, { - type Output = Message; + type Output = T; fn hash(&self, state: &mut Hasher) { std::any::TypeId::of::<I>().hash(state); diff --git a/src/lib.rs b/src/lib.rs index bc3fe6ab..09d9860e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,6 +175,7 @@ use iced_winit::core; use iced_winit::runtime; pub use iced_futures::futures; +pub use iced_futures::stream; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; @@ -202,6 +203,7 @@ pub use crate::core::{ Theme, Transformation, Vector, }; pub use crate::runtime::{exit, Task}; +pub use iced_futures::Subscription; pub mod clipboard { //! Access the clipboard. @@ -255,13 +257,6 @@ pub mod mouse { }; } -pub mod subscription { - //! Listen to external events in your application. - pub use iced_futures::subscription::{ - channel, run, run_with_id, unfold, Subscription, - }; -} - #[cfg(feature = "system")] pub mod system { //! Retrieve system information. @@ -314,7 +309,6 @@ pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; pub use settings::Settings; -pub use subscription::Subscription; #[doc(inline)] pub use application::application; diff --git a/winit/src/program.rs b/winit/src/program.rs index e1693196..3a4e2e48 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -207,7 +207,9 @@ where runtime.run(stream); } - runtime.track(program.subscription().map(Action::Output).into_recipes()); + runtime.track(subscription::into_recipes( + program.subscription().map(Action::Output), + )); let (boot_sender, boot_receiver) = oneshot::channel(); let (event_sender, event_receiver) = mpsc::unbounded(); @@ -1120,7 +1122,7 @@ fn update<P: Program, E: Executor>( } let subscription = program.subscription(); - runtime.track(subscription.map(Action::Output).into_recipes()); + runtime.track(subscription::into_recipes(subscription.map(Action::Output))); } fn run_action<P, C>( From 0cf096273aa71f9c786c4edaa64507ab42c5bca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 5 Jul 2024 02:19:17 +0200 Subject: [PATCH 079/657] Fix `wasm_bindgen` backend in `iced_futures` --- futures/src/backend/wasm/wasm_bindgen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs index 3228dd18..f7846c01 100644 --- a/futures/src/backend/wasm/wasm_bindgen.rs +++ b/futures/src/backend/wasm/wasm_bindgen.rs @@ -26,7 +26,7 @@ pub mod time { pub fn every( duration: std::time::Duration, ) -> Subscription<wasm_timer::Instant> { - Subscription::from_recipe(Every(duration)) + subscription::from_recipe(Every(duration)) } #[derive(Debug)] From c9e0ed7ca4a7fce23450b9aeba6eb79244832521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 5 Jul 2024 02:22:56 +0200 Subject: [PATCH 080/657] Expose `from_recipe` and `into_recipes` in `advanced::subscription` --- src/advanced.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/advanced.rs b/src/advanced.rs index 8d06e805..b817bbf9 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -14,6 +14,6 @@ pub use crate::renderer::graphics; pub mod subscription { //! Write your own subscriptions. pub use crate::runtime::futures::subscription::{ - EventStream, Hasher, Recipe, + from_recipe, into_recipes, EventStream, Hasher, Recipe, }; } From 23d9497e7ff54f48a4c07247d7f1f68270b510d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 8 Jul 2024 01:13:22 +0200 Subject: [PATCH 081/657] Allow future in `stream::channel` to return --- futures/src/stream.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/futures/src/stream.rs b/futures/src/stream.rs index 2ec505f1..06f9230d 100644 --- a/futures/src/stream.rs +++ b/futures/src/stream.rs @@ -1,6 +1,5 @@ //! Create asynchronous streams of data. use futures::channel::mpsc; -use futures::never::Never; use futures::stream::{self, Stream, StreamExt}; use std::future::Future; @@ -16,11 +15,11 @@ pub fn channel<T, F>( f: impl FnOnce(mpsc::Sender<T>) -> F, ) -> impl Stream<Item = T> where - F: Future<Output = Never>, + F: Future<Output = ()>, { let (sender, receiver) = mpsc::channel(size); - let runner = stream::once(f(sender)).map(|_| unreachable!()); + let runner = stream::once(f(sender)).filter_map(|_| async { None }); stream::select(receiver, runner) } From 76f5bc2ccebb4c8b76ef81856c52da5b5e7c8960 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:47:38 -0300 Subject: [PATCH 082/657] add SelectAll to TextEditor --- core/src/text/editor.rs | 2 ++ graphics/src/text/editor.rs | 21 +++++++++++++++++++++ widget/src/text_editor.rs | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index fbf60696..aea00921 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -70,6 +70,8 @@ pub enum Action { SelectWord, /// Select the line at the current cursor. SelectLine, + /// Select the entire buffer. + SelectAll, /// Perform an [`Edit`]. Edit(Edit), /// Click the [`Editor`] at the given [`Point`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c488a51c..36b4ca6e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -385,6 +385,27 @@ impl editor::Editor for Editor { })); } } + Action::SelectAll => { + let buffer = editor.buffer(); + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) + { + let cursor = editor.cursor(); + editor.set_select_opt(Some(cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + })); + + editor.action( + font_system.raw(), + motion_to_action(Motion::DocumentEnd), + ); + } + } // Editing events Action::Edit(edit) => { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index fc2ade43..0156b960 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -762,6 +762,11 @@ impl Update { { return Some(Self::Paste); } + keyboard::Key::Character("a") + if modifiers.command() => + { + return Some(Self::Action(Action::SelectAll)); + } _ => {} } From 3d99da805dd42a062aa66a3bdc43c7cf82fa4fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 9 Jul 2024 00:27:59 +0200 Subject: [PATCH 083/657] Implement `Default` for `combo_box::State` --- widget/src/combo_box.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 253850df..0a4624cb 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -280,6 +280,15 @@ where } } +impl<T> Default for State<T> +where + T: Display + Clone, +{ + fn default() -> Self { + Self::new(Vec::new()) + } +} + impl<T> Filtered<T> where T: Clone, From e86920be5b9984b4eb511e5e69efdcbf6ef3d8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 9 Jul 2024 00:28:40 +0200 Subject: [PATCH 084/657] Remove `load` method from `application` and `daemon` If you need to run a `Task` during boot, use `run_with` instead! --- examples/download_progress/Cargo.toml | 2 +- examples/editor/src/main.rs | 39 ++++----- examples/multi_window/src/main.rs | 16 ++-- examples/pokedex/Cargo.toml | 2 +- examples/pokedex/src/main.rs | 14 ++-- examples/todos/src/main.rs | 13 +-- examples/websocket/src/main.rs | 27 +++---- src/application.rs | 22 +---- src/daemon.rs | 21 +---- src/program.rs | 111 +++----------------------- 10 files changed, 70 insertions(+), 197 deletions(-) diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index 18a49f66..f78df529 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -10,6 +10,6 @@ iced.workspace = true iced.features = ["tokio"] [dependencies.reqwest] -version = "0.11" +version = "0.12" default-features = false features = ["rustls-tls"] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index bed9d94a..ce3c478d 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -13,12 +13,11 @@ use std::sync::Arc; pub fn main() -> iced::Result { iced::application("Editor - Iced", Editor::update, Editor::view) - .load(Editor::load) .subscription(Editor::subscription) .theme(Editor::theme) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .default_font(Font::MONOSPACE) - .run() + .run_with(Editor::new) } struct Editor { @@ -41,20 +40,22 @@ enum Message { } impl Editor { - fn new() -> Self { - Self { - file: None, - content: text_editor::Content::new(), - theme: highlighter::Theme::SolarizedDark, - is_loading: true, - is_dirty: false, - } - } - - fn load() -> Task<Message> { - Task::perform( - load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))), - Message::FileOpened, + fn new() -> (Self, Task<Message>) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + }, + Task::perform( + load_file(format!( + "{}/src/main.rs", + env!("CARGO_MANIFEST_DIR") + )), + Message::FileOpened, + ), ) } @@ -214,12 +215,6 @@ impl Editor { } } -impl Default for Editor { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Clone)] pub enum Error { DialogClosed, diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 98e753ab..460ca3b5 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -9,16 +9,12 @@ use std::collections::BTreeMap; fn main() -> iced::Result { iced::daemon(Example::title, Example::update, Example::view) - .load(|| { - window::open(window::Settings::default()).map(Message::WindowOpened) - }) .subscription(Example::subscription) .theme(Example::theme) .scale_factor(Example::scale_factor) - .run() + .run_with(Example::new) } -#[derive(Default)] struct Example { windows: BTreeMap<window::Id, Window>, } @@ -43,6 +39,16 @@ enum Message { } impl Example { + fn new() -> (Self, Task<Message>) { + ( + Self { + windows: BTreeMap::new(), + }, + window::open(window::Settings::default()) + .map(Message::WindowOpened), + ) + } + fn title(&self, window: window::Id) -> String { self.windows .get(&window) diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index bf7e1e35..1a6d5445 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -16,7 +16,7 @@ version = "1.0" features = ["derive"] [dependencies.reqwest] -version = "0.11" +version = "0.12" default-features = false features = ["json", "rustls-tls"] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index b22ffe7f..7414ae54 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -4,17 +4,13 @@ use iced::{Alignment, Element, Length, Task}; pub fn main() -> iced::Result { iced::application(Pokedex::title, Pokedex::update, Pokedex::view) - .load(Pokedex::search) - .run() + .run_with(Pokedex::new) } -#[derive(Debug, Default)] +#[derive(Debug)] enum Pokedex { - #[default] Loading, - Loaded { - pokemon: Pokemon, - }, + Loaded { pokemon: Pokemon }, Errored, } @@ -25,6 +21,10 @@ enum Message { } impl Pokedex { + fn new() -> (Self, Task<Message>) { + (Self::Loading, Self::search()) + } + fn search() -> Task<Message> { Task::perform(Pokemon::search(), Message::PokemonFound) } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6ed50d31..b34f71ce 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -18,16 +18,14 @@ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); iced::application(Todos::title, Todos::update, Todos::view) - .load(Todos::load) .subscription(Todos::subscription) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .window_size((500.0, 800.0)) - .run() + .run_with(Todos::new) } -#[derive(Default, Debug)] +#[derive(Debug)] enum Todos { - #[default] Loading, Loaded(State), } @@ -54,8 +52,11 @@ enum Message { } impl Todos { - fn load() -> Command<Message> { - Command::perform(SavedState::load(), Message::Loaded) + fn new() -> (Self, Command<Message>) { + ( + Self::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) } fn title(&self) -> String { diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 95a14fd9..d8246436 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -9,12 +9,10 @@ use once_cell::sync::Lazy; pub fn main() -> iced::Result { iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view) - .load(WebSocket::load) .subscription(WebSocket::subscription) - .run() + .run_with(WebSocket::new) } -#[derive(Default)] struct WebSocket { messages: Vec<echo::Message>, new_message: String, @@ -30,11 +28,18 @@ enum Message { } impl WebSocket { - fn load() -> Task<Message> { - Task::batch([ - Task::perform(echo::server::run(), |_| Message::Server), - widget::focus_next(), - ]) + fn new() -> (Self, Task<Message>) { + ( + Self { + messages: Vec::new(), + new_message: String::new(), + state: State::Disconnected, + }, + Task::batch([ + Task::perform(echo::server::run(), |_| Message::Server), + widget::focus_next(), + ]), + ) } fn update(&mut self, message: Message) -> Task<Message> { @@ -140,10 +145,4 @@ enum State { Connected(echo::Connection), } -impl Default for State { - fn default() -> Self { - Self::Disconnected - } -} - static MESSAGE_LOG: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique); diff --git a/src/application.rs b/src/application.rs index 5d16b40f..f5e06471 100644 --- a/src/application.rs +++ b/src/application.rs @@ -103,10 +103,6 @@ where type Renderer = Renderer; type Executor = iced_futures::backend::default::Executor; - fn load(&self) -> Task<Self::Message> { - Task::none() - } - fn update( &self, state: &mut Self::State, @@ -166,14 +162,14 @@ impl<P: Program> Application<P> { Self: 'static, P::State: Default, { - self.run_with(P::State::default) + self.raw.run(self.settings, Some(self.window)) } /// Runs the [`Application`] with a closure that creates the initial state. pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> P::State + Clone + 'static, + I: Fn() -> (P::State, Task<P::Message>) + Clone + 'static, { self.raw .run_with(self.settings, Some(self.window), initialize) @@ -323,20 +319,6 @@ impl<P: Program> Application<P> { } } - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Application< - impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Application { - raw: program::with_load(self.raw, f), - settings: self.settings, - window: self.window, - } - } - /// Sets the subscription logic of the [`Application`]. pub fn subscription( self, diff --git a/src/daemon.rs b/src/daemon.rs index 58293949..d2de2db7 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -55,10 +55,6 @@ where type Renderer = Renderer; type Executor = iced_futures::backend::default::Executor; - fn load(&self) -> Task<Self::Message> { - Task::none() - } - fn update( &self, state: &mut Self::State, @@ -116,14 +112,14 @@ impl<P: Program> Daemon<P> { Self: 'static, P::State: Default, { - self.run_with(P::State::default) + self.raw.run(self.settings, None) } /// Runs the [`Daemon`] with a closure that creates the initial state. pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> P::State + Clone + 'static, + I: Fn() -> (P::State, Task<P::Message>) + Clone + 'static, { self.raw.run_with(self.settings, None, initialize) } @@ -176,19 +172,6 @@ impl<P: Program> Daemon<P> { } } - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Daemon< - impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Daemon { - raw: program::with_load(self.raw, f), - settings: self.settings, - } - } - /// Sets the subscription logic of the [`Daemon`]. pub fn subscription( self, diff --git a/src/program.rs b/src/program.rs index 3f9d2d0c..939b0047 100644 --- a/src/program.rs +++ b/src/program.rs @@ -27,8 +27,6 @@ pub trait Program: Sized { /// The executor of the program. type Executor: Executor; - fn load(&self) -> Task<Self::Message>; - fn update( &self, state: &mut Self::State, @@ -80,7 +78,9 @@ pub trait Program: Sized { Self: 'static, Self::State: Default, { - self.run_with(settings, window_settings, Self::State::default) + self.run_with(settings, window_settings, || { + (Self::State::default(), Task::none()) + }) } /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. @@ -92,7 +92,7 @@ pub trait Program: Sized { ) -> Result where Self: 'static, - I: Fn() -> Self::State + Clone + 'static, + I: Fn() -> (Self::State, Task<Self::Message>) + Clone + 'static, { use std::marker::PhantomData; @@ -102,7 +102,9 @@ pub trait Program: Sized { _initialize: PhantomData<I>, } - impl<P: Program, I: Fn() -> P::State> shell::Program for Instance<P, I> { + impl<P: Program, I: Fn() -> (P::State, Task<P::Message>)> shell::Program + for Instance<P, I> + { type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; @@ -112,8 +114,7 @@ pub trait Program: Sized { fn new( (program, initialize): Self::Flags, ) -> (Self, Task<Self::Message>) { - let state = initialize(); - let command = program.load(); + let (state, task) = initialize(); ( Self { @@ -121,7 +122,7 @@ pub trait Program: Sized { state, _initialize: PhantomData, }, - command, + task, ) } @@ -212,10 +213,6 @@ pub fn with_title<P: Program>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { (self.title)(state, window) } @@ -267,80 +264,6 @@ pub fn with_title<P: Program>( WithTitle { program, title } } -pub fn with_load<P: Program>( - program: P, - f: impl Fn() -> Task<P::Message>, -) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { - struct WithLoad<P, F> { - program: P, - load: F, - } - - impl<P: Program, F> Program for WithLoad<P, F> - where - F: Fn() -> Task<P::Message>, - { - type State = P::State; - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Executor = P::Executor; - - fn load(&self) -> Task<Self::Message> { - Task::batch([self.program.load(), (self.load)()]) - } - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task<Self::Message> { - self.program.update(state, message) - } - - fn view<'a>( - &self, - state: &'a Self::State, - window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state, window) - } - - fn title(&self, state: &Self::State, window: window::Id) -> String { - self.program.title(state, window) - } - - fn subscription( - &self, - state: &Self::State, - ) -> Subscription<Self::Message> { - self.program.subscription(state) - } - - fn theme( - &self, - state: &Self::State, - window: window::Id, - ) -> Self::Theme { - self.program.theme(state, window) - } - - fn style( - &self, - state: &Self::State, - theme: &Self::Theme, - ) -> Appearance { - self.program.style(state, theme) - } - - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { - self.program.scale_factor(state, window) - } - } - - WithLoad { program, load: f } -} - pub fn with_subscription<P: Program>( program: P, f: impl Fn(&P::State) -> Subscription<P::Message>, @@ -367,10 +290,6 @@ pub fn with_subscription<P: Program>( (self.subscription)(state) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn update( &self, state: &mut Self::State, @@ -445,10 +364,6 @@ pub fn with_theme<P: Program>( (self.theme)(state, window) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -519,10 +434,6 @@ pub fn with_style<P: Program>( (self.style)(state, theme) } - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -585,10 +496,6 @@ pub fn with_scale_factor<P: Program>( type Renderer = P::Renderer; type Executor = P::Executor; - fn load(&self) -> Task<Self::Message> { - self.program.load() - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } From 8efe161e3d08b56cba8db1320b8433efa45fa79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 10 Jul 2024 14:24:52 +0200 Subject: [PATCH 085/657] Move docs of `future` and `stream` in `Task` --- runtime/src/task.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index e037c403..d1864473 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -55,24 +55,6 @@ impl<T> Task<T> { Self::stream(stream.map(f)) } - /// Creates a new [`Task`] that runs the given [`Future`] and produces - /// its output. - pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self::stream(stream::once(future)) - } - - /// Creates a new [`Task`] that runs the given [`Stream`] and produces - /// each of its items. - pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self - where - T: 'static, - { - Self(Some(boxed_stream(stream.map(Action::Output)))) - } - /// Combines the given tasks and produces a single [`Task`] that will run all of them /// in parallel. pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self @@ -176,6 +158,24 @@ impl<T> Task<T> { ))), } } + + /// Creates a new [`Task`] that runs the given [`Future`] and produces + /// its output. + pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self::stream(stream::once(future)) + } + + /// Creates a new [`Task`] that runs the given [`Stream`] and produces + /// each of its items. + pub fn stream(stream: impl Stream<Item = T> + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream.map(Action::Output)))) + } } impl<T> Task<Option<T>> { From 47f9554a82e65679c13ef17f3f3bf7fff5156184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 10 Jul 2024 14:40:58 +0200 Subject: [PATCH 086/657] Introduce `Task::abortable` :tada: --- runtime/src/task.rs | 37 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 +++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index d1864473..b75aca89 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -159,6 +159,21 @@ impl<T> Task<T> { } } + /// Creates a new [`Task`] that can be aborted with the returned [`Handle`]. + pub fn abortable(self) -> (Self, Handle) + where + T: 'static, + { + match self.0 { + Some(stream) => { + let (stream, handle) = stream::abortable(stream); + + (Self(Some(boxed_stream(stream))), Handle(Some(handle))) + } + None => (Self(None), Handle(None)), + } + } + /// Creates a new [`Task`] that runs the given [`Future`] and produces /// its output. pub fn future(future: impl Future<Output = T> + MaybeSend + 'static) -> Self @@ -178,6 +193,28 @@ impl<T> Task<T> { } } +/// A handle to a [`Task`] that can be used for aborting it. +#[derive(Debug, Clone)] +pub struct Handle(Option<stream::AbortHandle>); + +impl Handle { + /// Aborts the [`Task`] of this [`Handle`]. + pub fn abort(&self) { + if let Some(handle) = &self.0 { + handle.abort(); + } + } + + /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted. + pub fn is_aborted(&self) -> bool { + if let Some(handle) = &self.0 { + handle.is_aborted() + } else { + true + } + } +} + impl<T> Task<Option<T>> { /// Executes a new [`Task`] after this one, only when it produces `Some` value. /// diff --git a/src/lib.rs b/src/lib.rs index 09d9860e..79e2f276 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,9 +202,14 @@ pub use crate::core::{ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; -pub use crate::runtime::{exit, Task}; +pub use crate::runtime::exit; pub use iced_futures::Subscription; +pub mod task { + //! Create runtime tasks. + pub use crate::runtime::task::{Handle, Task}; +} + pub mod clipboard { //! Access the clipboard. pub use crate::runtime::clipboard::{ @@ -309,6 +314,7 @@ pub use executor::Executor; pub use font::Font; pub use renderer::Renderer; pub use settings::Settings; +pub use task::Task; #[doc(inline)] pub use application::application; From 4ce2a207a6c2f28345dc86804a12f2faab3d07ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 04:08:40 +0200 Subject: [PATCH 087/657] Introduce `stream::try_channel` helper --- futures/src/stream.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/futures/src/stream.rs b/futures/src/stream.rs index 06f9230d..af2f8c99 100644 --- a/futures/src/stream.rs +++ b/futures/src/stream.rs @@ -23,3 +23,24 @@ where stream::select(receiver, runner) } + +/// Creates a new [`Stream`] that produces the items sent from a [`Future`] +/// that can fail to the [`mpsc::Sender`] provided to the closure. +pub fn try_channel<T, E, F>( + size: usize, + f: impl FnOnce(mpsc::Sender<T>) -> F, +) -> impl Stream<Item = Result<T, E>> +where + F: Future<Output = Result<(), E>>, +{ + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|result| async { + match result { + Ok(()) => None, + Err(error) => Some(Err(error)), + } + }); + + stream::select(receiver.map(Ok), runner) +} From 5e6c9eeb7e6f9ee985257853a140c19ec7ca1de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 04:33:19 +0200 Subject: [PATCH 088/657] Add `iced` widget helper to display the iced logo :comet: --- examples/slider/Cargo.toml | 1 + examples/slider/src/main.rs | 43 ++++++++++++----------------------- widget/Cargo.toml | 1 + widget/assets/iced-logo.svg | 2 ++ widget/src/helpers.rs | 35 ++++++++++++++++++++++++++++ widget/src/slider.rs | 2 +- widget/src/vertical_slider.rs | 2 +- 7 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 widget/assets/iced-logo.svg diff --git a/examples/slider/Cargo.toml b/examples/slider/Cargo.toml index fad8916e..05e74d2c 100644 --- a/examples/slider/Cargo.toml +++ b/examples/slider/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] iced.workspace = true +iced.features = ["svg"] diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index 0b4c29aa..fd312763 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{center, column, container, slider, text, vertical_slider}; -use iced::{Element, Length}; +use iced::widget::{column, container, iced, slider, text, vertical_slider}; +use iced::{Alignment, Element, Length}; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -12,19 +12,11 @@ pub enum Message { pub struct Slider { value: u8, - default: u8, - step: u8, - shift_step: u8, } impl Slider { fn new() -> Self { - Slider { - value: 50, - default: 50, - step: 5, - shift_step: 1, - } + Slider { value: 50 } } fn update(&mut self, message: Message) { @@ -37,32 +29,27 @@ impl Slider { fn view(&self) -> Element<Message> { let h_slider = container( - slider(0..=100, self.value, Message::SliderChanged) - .default(self.default) - .step(self.step) - .shift_step(self.shift_step), + slider(1..=100, self.value, Message::SliderChanged) + .default(50) + .shift_step(5), ) .width(250); let v_slider = container( - vertical_slider(0..=100, self.value, Message::SliderChanged) - .default(self.default) - .step(self.step) - .shift_step(self.shift_step), + vertical_slider(1..=100, self.value, Message::SliderChanged) + .default(50) + .shift_step(5), ) .height(200); let text = text(self.value); - center( - column![ - container(v_slider).center_x(Length::Fill), - container(h_slider).center_x(Length::Fill), - container(text).center_x(Length::Fill) - ] - .spacing(25), - ) - .into() + column![v_slider, h_slider, text, iced(self.value as f32),] + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(20) + .padding(20) + .into() } } diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3c9f6a54..498a768b 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -31,6 +31,7 @@ iced_renderer.workspace = true iced_runtime.workspace = true num-traits.workspace = true +once_cell.workspace = true rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true diff --git a/widget/assets/iced-logo.svg b/widget/assets/iced-logo.svg new file mode 100644 index 00000000..459b7fbb --- /dev/null +++ b/widget/assets/iced-logo.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg> diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 9056d191..d7631959 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -890,6 +890,41 @@ where crate::Svg::new(handle) } +/// Creates an [`Element`] that displays the iced logo with the given `text_size`. +/// +/// Useful for showing some love to your favorite GUI library in your "About" screen, +/// for instance. +#[cfg(feature = "svg")] +pub fn iced<'a, Message, Theme, Renderer>( + text_size: impl Into<Pixels>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: core::Renderer + + core::text::Renderer<Font = core::Font> + + core::svg::Renderer + + 'a, + Theme: text::Catalog + crate::svg::Catalog + 'a, +{ + use crate::core::{Alignment, Font}; + use crate::svg; + use once_cell::sync::Lazy; + + static LOGO: Lazy<svg::Handle> = Lazy::new(|| { + svg::Handle::from_memory(include_bytes!("../assets/iced-logo.svg")) + }); + + let text_size = text_size.into(); + + row![ + svg(LOGO.clone()).width(text_size * 1.3), + text("iced").size(text_size).font(Font::MONOSPACE) + ] + .spacing(text_size.0 / 3.0) + .align_items(Alignment::Center) + .into() +} + /// Creates a new [`Canvas`]. /// /// [`Canvas`]: crate::Canvas diff --git a/widget/src/slider.rs b/widget/src/slider.rs index a8f1d192..74e6f8d3 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -237,7 +237,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index defb442f..33c591f5 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -239,7 +239,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value From bec3ca56c305bfcf2e1d4305c9175824b9e40e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 04:37:03 +0200 Subject: [PATCH 089/657] Add `align_x` and `align_y` helpers to `Scrollable` --- widget/src/scrollable.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 3ff1f8a1..62f8fcfe 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -109,6 +109,32 @@ where self } + /// Inverts the alignment of the horizontal direction of the [`Scrollable`], if applicable. + pub fn align_x(mut self, alignment: Alignment) -> Self { + match &mut self.direction { + Direction::Horizontal(horizontal) + | Direction::Both { horizontal, .. } => { + horizontal.alignment = alignment; + } + Direction::Vertical(_) => {} + } + + self + } + + /// Sets the alignment of the vertical direction of the [`Scrollable`], if applicable. + pub fn align_y(mut self, alignment: Alignment) -> Self { + match &mut self.direction { + Direction::Vertical(vertical) + | Direction::Both { vertical, .. } => { + vertical.alignment = alignment; + } + Direction::Horizontal(_) => {} + } + + self + } + /// Sets the style of this [`Scrollable`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self From 03e8078f4266e73f5b0fe5bc18c8853f947417bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 04:57:40 +0200 Subject: [PATCH 090/657] Add some built-in text styles for each `Palette` color --- core/src/widget/text.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index f1f0b345..081407e5 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -367,6 +367,34 @@ impl Catalog for Theme { } } +/// Text conveying some important information, like an action. +pub fn primary(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().primary), + } +} + +/// Text conveying some secondary information, like a footnote. +pub fn secondary(theme: &Theme) -> Style { + Style { + color: Some(theme.extended_palette().secondary.strong.color), + } +} + +/// Text conveying some positive information, like a successful event. +pub fn success(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().success), + } +} + +/// Text conveying some negative information, like an error. +pub fn danger(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().danger), + } +} + /// A fragment of [`Text`]. /// /// This is just an alias to a string that may be either From 8ae4e09db9badb801669c15408bc76e8675f9cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 07:58:33 +0200 Subject: [PATCH 091/657] Add support for embedded scrollbars for `scrollable` Co-authored-by: dtzxporter <dtzxporter@users.noreply.github.com> --- core/src/padding.rs | 36 ++- examples/scrollable/src/main.rs | 61 +++-- widget/src/overlay/menu.rs | 27 +-- widget/src/scrollable.rs | 413 +++++++++++++++++++------------- 4 files changed, 325 insertions(+), 212 deletions(-) diff --git a/core/src/padding.rs b/core/src/padding.rs index a63f6e29..b8c941d8 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -1,4 +1,4 @@ -use crate::Size; +use crate::{Pixels, Size}; /// An amount of space to pad for each side of a box /// @@ -54,7 +54,7 @@ impl Padding { left: 0.0, }; - /// Create a Padding that is equal on all sides + /// Create a [`Padding`] that is equal on all sides. pub const fn new(padding: f32) -> Padding { Padding { top: padding, @@ -64,6 +64,38 @@ impl Padding { } } + /// Create some top [`Padding`]. + pub fn top(padding: impl Into<Pixels>) -> Self { + Self { + top: padding.into().0, + ..Self::ZERO + } + } + + /// Create some right [`Padding`]. + pub fn right(padding: impl Into<Pixels>) -> Self { + Self { + right: padding.into().0, + ..Self::ZERO + } + } + + /// Create some bottom [`Padding`]. + pub fn bottom(padding: impl Into<Pixels>) -> Self { + Self { + bottom: padding.into().0, + ..Self::ZERO + } + } + + /// Create some left [`Padding`]. + pub fn left(padding: impl Into<Pixels>) -> Self { + Self { + left: padding.into().0, + ..Self::ZERO + } + } + /// Returns the total amount of vertical [`Padding`]. pub fn vertical(self) -> f32 { self.top + self.bottom diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index f2a853e1..067dcd70 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,7 +1,6 @@ -use iced::widget::scrollable::Properties; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, - scrollable, slider, text, vertical_space, Scrollable, + scrollable, slider, text, vertical_space, }; use iced::{Alignment, Border, Color, Element, Length, Task, Theme}; @@ -203,7 +202,7 @@ impl ScrollableDemo { let scrollable_content: Element<Message> = Element::from(match self.scrollable_direction { - Direction::Vertical => Scrollable::with_direction( + Direction::Vertical => scrollable( column![ scroll_to_end_button(), text("Beginning!"), @@ -216,19 +215,19 @@ impl ScrollableDemo { .align_items(Alignment::Center) .padding([40, 0, 40, 0]) .spacing(40), - scrollable::Direction::Vertical( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment), - ), ) + .direction(scrollable::Direction::Vertical( + scrollable::Scrollbar::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .alignment(self.alignment), + )) .width(Length::Fill) .height(Length::Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), - Direction::Horizontal => Scrollable::with_direction( + Direction::Horizontal => scrollable( row![ scroll_to_end_button(), text("Beginning!"), @@ -242,19 +241,19 @@ impl ScrollableDemo { .align_items(Alignment::Center) .padding([0, 40, 0, 40]) .spacing(40), - scrollable::Direction::Horizontal( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment), - ), ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .alignment(self.alignment), + )) .width(Length::Fill) .height(Length::Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), - Direction::Multi => Scrollable::with_direction( + Direction::Multi => scrollable( //horizontal content row![ column![ @@ -284,19 +283,19 @@ impl ScrollableDemo { .align_items(Alignment::Center) .padding([0, 40, 0, 40]) .spacing(40), - { - let properties = Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment); - - scrollable::Direction::Both { - horizontal: properties, - vertical: properties, - } - }, ) + .direction({ + let scrollbar = scrollable::Scrollbar::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .alignment(self.alignment); + + scrollable::Direction::Both { + horizontal: scrollbar, + vertical: scrollbar, + } + }) .width(Length::Fill) .height(Length::Fill) .id(SCROLLABLE_ID.clone()) diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 98efe305..a43c8e8b 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -200,21 +200,18 @@ where class, } = menu; - let list = Scrollable::with_direction( - List { - options, - hovered_option, - on_selected, - on_option_hovered, - font, - text_size, - text_line_height, - text_shaping, - padding, - class, - }, - scrollable::Direction::default(), - ); + let list = Scrollable::new(List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + class, + }); state.tree.diff(&list as &dyn Widget<_, _, _>); diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e0875bbf..35613910 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -12,7 +12,7 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, - Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -49,37 +49,38 @@ where pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Self { - Self::with_direction(content, Direction::default()) - } - - /// Creates a new [`Scrollable`] with the given [`Direction`]. - pub fn with_direction( - content: impl Into<Element<'a, Message, Theme, Renderer>>, - direction: Direction, - ) -> Self { - let content = content.into(); - - debug_assert!( - direction.vertical().is_none() - || !content.as_widget().size_hint().height.is_fill(), - "scrollable content must not fill its vertical scrolling axis" - ); - - debug_assert!( - direction.horizontal().is_none() - || !content.as_widget().size_hint().width.is_fill(), - "scrollable content must not fill its horizontal scrolling axis" - ); - Scrollable { id: None, width: Length::Shrink, height: Length::Shrink, - direction, - content, + direction: Direction::default(), + content: content.into(), on_scroll: None, class: Theme::default(), } + .validate() + } + + fn validate(self) -> Self { + debug_assert!( + self.direction.vertical().is_none() + || !self.content.as_widget().size_hint().height.is_fill(), + "scrollable content must not fill its vertical scrolling axis" + ); + + debug_assert!( + self.direction.horizontal().is_none() + || !self.content.as_widget().size_hint().width.is_fill(), + "scrollable content must not fill its horizontal scrolling axis" + ); + + self + } + + /// Creates a new [`Scrollable`] with the given [`Direction`]. + pub fn direction(mut self, direction: impl Into<Direction>) -> Self { + self.direction = direction.into(); + self.validate() } /// Sets the [`Id`] of the [`Scrollable`]. @@ -108,7 +109,7 @@ where self } - /// Inverts the alignment of the horizontal direction of the [`Scrollable`], if applicable. + /// Sets the alignment of the horizontal direction of the [`Scrollable`], if applicable. pub fn align_x(mut self, alignment: Alignment) -> Self { match &mut self.direction { Direction::Horizontal(horizontal) @@ -134,6 +135,32 @@ where self } + /// Sets whether the horizontal [`Scrollbar`] should be embedded in the [`Scrollable`]. + pub fn embed_x(mut self, embedded: bool) -> Self { + match &mut self.direction { + Direction::Horizontal(horizontal) + | Direction::Both { horizontal, .. } => { + horizontal.embedded = embedded; + } + Direction::Vertical(_) => {} + } + + self + } + + /// Sets whether the vertical [`Scrollbar`] should be embedded in the [`Scrollable`]. + pub fn embed_y(mut self, embedded: bool) -> Self { + match &mut self.direction { + Direction::Vertical(vertical) + | Direction::Both { vertical, .. } => { + vertical.embedded = embedded; + } + Direction::Horizontal(_) => {} + } + + self + } + /// Sets the style of this [`Scrollable`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -157,21 +184,21 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub enum Direction { /// Vertical scrolling - Vertical(Properties), + Vertical(Scrollbar), /// Horizontal scrolling - Horizontal(Properties), + Horizontal(Scrollbar), /// Both vertical and horizontal scrolling Both { /// The properties of the vertical scrollbar. - vertical: Properties, + vertical: Scrollbar, /// The properties of the horizontal scrollbar. - horizontal: Properties, + horizontal: Scrollbar, }, } impl Direction { /// Returns the [`Properties`] of the horizontal scrollbar, if any. - pub fn horizontal(&self) -> Option<&Properties> { + pub fn horizontal(&self) -> Option<&Scrollbar> { match self { Self::Horizontal(properties) => Some(properties), Self::Both { horizontal, .. } => Some(horizontal), @@ -180,7 +207,7 @@ impl Direction { } /// Returns the [`Properties`] of the vertical scrollbar, if any. - pub fn vertical(&self) -> Option<&Properties> { + pub fn vertical(&self) -> Option<&Scrollbar> { match self { Self::Vertical(properties) => Some(properties), Self::Both { vertical, .. } => Some(vertical), @@ -191,31 +218,33 @@ impl Direction { impl Default for Direction { fn default() -> Self { - Self::Vertical(Properties::default()) + Self::Vertical(Scrollbar::default()) } } /// Properties of a scrollbar within a [`Scrollable`]. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Properties { +pub struct Scrollbar { width: f32, margin: f32, scroller_width: f32, alignment: Alignment, + embedded: bool, } -impl Default for Properties { +impl Default for Scrollbar { fn default() -> Self { Self { width: 10.0, margin: 0.0, scroller_width: 10.0, alignment: Alignment::Start, + embedded: false, } } } -impl Properties { +impl Scrollbar { /// Creates new [`Properties`] for use in a [`Scrollable`]. pub fn new() -> Self { Self::default() @@ -244,6 +273,15 @@ impl Properties { self.alignment = alignment; self } + + /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`]. + /// + /// An embedded [`Scrollbar`] will always be displayed, will take layout space, + /// and will not float over the contents. + pub fn embedded(mut self, embedded: bool) -> Self { + self.embedded = embedded; + self + } } /// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction. @@ -291,29 +329,49 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::contained(limits, self.width, self.height, |limits| { - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if self.direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if self.direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); + let (right_padding, bottom_padding) = match self.direction { + Direction::Vertical(scrollbar) if scrollbar.embedded => { + (scrollbar.width + scrollbar.margin * 2.0, 0.0) + } + Direction::Horizontal(scrollbar) if scrollbar.embedded => { + (0.0, scrollbar.width + scrollbar.margin * 2.0) + } + _ => (0.0, 0.0), + }; - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - &child_limits, - ) - }) + layout::padded( + limits, + self.width, + self.height, + Padding { + right: right_padding, + bottom: bottom_padding, + ..Padding::ZERO + }, + |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if self.direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if self.direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); + + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + &child_limits, + ) + }, + ) } fn operate( @@ -762,7 +820,7 @@ where let draw_scrollbar = |renderer: &mut Renderer, - style: Scrollbar, + style: Rail, scrollbar: &internals::Scrollbar| { if scrollbar.bounds.width > 0.0 && scrollbar.bounds.height > 0.0 @@ -782,21 +840,23 @@ where ); } - if scrollbar.scroller.bounds.width > 0.0 - && scrollbar.scroller.bounds.height > 0.0 - && (style.scroller.color != Color::TRANSPARENT - || (style.scroller.border.color - != Color::TRANSPARENT - && style.scroller.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border: style.scroller.border, - ..renderer::Quad::default() - }, - style.scroller.color, - ); + if let Some(scroller) = scrollbar.scroller { + if scroller.bounds.width > 0.0 + && scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border.color + != Color::TRANSPARENT + && style.scroller.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scroller.bounds, + border: style.scroller.border, + ..renderer::Quad::default() + }, + style.scroller.color, + ); + } } }; @@ -810,7 +870,7 @@ where if let Some(scrollbar) = scrollbars.y { draw_scrollbar( renderer, - style.vertical_scrollbar, + style.vertical_rail, &scrollbar, ); } @@ -818,7 +878,7 @@ where if let Some(scrollbar) = scrollbars.x { draw_scrollbar( renderer, - style.horizontal_scrollbar, + style.horizontal_rail, &scrollbar, ); } @@ -1324,16 +1384,16 @@ impl Scrollbars { ) -> Self { let translation = state.translation(direction, bounds, content_bounds); - let show_scrollbar_x = direction - .horizontal() - .filter(|_| content_bounds.width > bounds.width); + let show_scrollbar_x = direction.horizontal().filter(|scrollbar| { + scrollbar.embedded || content_bounds.width > bounds.width + }); - let show_scrollbar_y = direction - .vertical() - .filter(|_| content_bounds.height > bounds.height); + let show_scrollbar_y = direction.vertical().filter(|scrollbar| { + scrollbar.embedded || content_bounds.height > bounds.height + }); let y_scrollbar = if let Some(vertical) = show_scrollbar_y { - let Properties { + let Scrollbar { width, margin, scroller_width, @@ -1367,26 +1427,35 @@ impl Scrollbars { }; let ratio = bounds.height / content_bounds.height; - // min height for easier grabbing with super tall content - let scroller_height = (scrollbar_bounds.height * ratio).max(2.0); - let scroller_offset = - translation.y * ratio * scrollbar_bounds.height / bounds.height; - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - scroller_width / 2.0, - y: (scrollbar_bounds.y + scroller_offset).max(0.0), - width: scroller_width, - height: scroller_height, + let scroller = if ratio >= 1.0 { + None + } else { + // min height for easier grabbing with super tall content + let scroller_height = + (scrollbar_bounds.height * ratio).max(2.0); + let scroller_offset = + translation.y * ratio * scrollbar_bounds.height + / bounds.height; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - total_scrollbar_width / 2.0 + - scroller_width / 2.0, + y: (scrollbar_bounds.y + scroller_offset).max(0.0), + width: scroller_width, + height: scroller_height, + }; + + Some(internals::Scroller { + bounds: scroller_bounds, + }) }; Some(internals::Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - scroller: internals::Scroller { - bounds: scroller_bounds, - }, + scroller, alignment: vertical.alignment, }) } else { @@ -1394,7 +1463,7 @@ impl Scrollbars { }; let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { - let Properties { + let Scrollbar { width, margin, scroller_width, @@ -1428,26 +1497,34 @@ impl Scrollbars { }; let ratio = bounds.width / content_bounds.width; - // min width for easier grabbing with extra wide content - let scroller_length = (scrollbar_bounds.width * ratio).max(2.0); - let scroller_offset = - translation.x * ratio * scrollbar_bounds.width / bounds.width; - let scroller_bounds = Rectangle { - x: (scrollbar_bounds.x + scroller_offset).max(0.0), - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - scroller_width / 2.0, - width: scroller_length, - height: scroller_width, + let scroller = if ratio >= 1.0 { + None + } else { + // min width for easier grabbing with extra wide content + let scroller_length = (scrollbar_bounds.width * ratio).max(2.0); + let scroller_offset = + translation.x * ratio * scrollbar_bounds.width + / bounds.width; + + let scroller_bounds = Rectangle { + x: (scrollbar_bounds.x + scroller_offset).max(0.0), + y: bounds.y + bounds.height + - total_scrollbar_height / 2.0 + - scroller_width / 2.0, + width: scroller_length, + height: scroller_width, + }; + + Some(internals::Scroller { + bounds: scroller_bounds, + }) }; Some(internals::Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - scroller: internals::Scroller { - bounds: scroller_bounds, - }, + scroller, alignment: horizontal.alignment, }) } else { @@ -1478,33 +1555,33 @@ impl Scrollbars { } fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> { - self.y.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.y - scrollbar.scroller.bounds.y) - / scrollbar.scroller.bounds.height - } else { - 0.5 - }) + let scrollbar = self.y?; + let scroller = scrollbar.scroller?; + + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scroller.bounds.contains(cursor_position) { + (cursor_position.y - scroller.bounds.y) / scroller.bounds.height } else { - None - } - }) + 0.5 + }) + } else { + None + } } fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> { - self.x.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.x - scrollbar.scroller.bounds.x) - / scrollbar.scroller.bounds.width - } else { - 0.5 - }) + let scrollbar = self.x?; + let scroller = scrollbar.scroller?; + + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scroller.bounds.contains(cursor_position) { + (cursor_position.x - scroller.bounds.x) / scroller.bounds.width } else { - None - } - }) + 0.5 + }) + } else { + None + } } fn active(&self) -> bool { @@ -1521,7 +1598,7 @@ pub(super) mod internals { pub struct Scrollbar { pub total_bounds: Rectangle, pub bounds: Rectangle, - pub scroller: Scroller, + pub scroller: Option<Scroller>, pub alignment: Alignment, } @@ -1537,14 +1614,18 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let percentage = (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height); + if let Some(scroller) = self.scroller { + let percentage = (cursor_position.y + - self.bounds.y + - scroller.bounds.height * grabbed_at) + / (self.bounds.height - scroller.bounds.height); - match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + match self.alignment { + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, + } + } else { + 0.0 } } @@ -1554,14 +1635,18 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let percentage = (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width); + if let Some(scroller) = self.scroller { + let percentage = (cursor_position.x + - self.bounds.x + - scroller.bounds.width * grabbed_at) + / (self.bounds.width - scroller.bounds.width); - match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + match self.alignment { + Alignment::Start => percentage, + Alignment::End => 1.0 - percentage, + } + } else { + 0.0 } } } @@ -1595,22 +1680,22 @@ pub enum Status { }, } -/// The appearance of a scrolable. +/// The appearance of a scrollable. #[derive(Debug, Clone, Copy)] pub struct Style { /// The [`container::Style`] of a scrollable. pub container: container::Style, - /// The vertical [`Scrollbar`] appearance. - pub vertical_scrollbar: Scrollbar, - /// The horizontal [`Scrollbar`] appearance. - pub horizontal_scrollbar: Scrollbar, + /// The vertical [`Rail`] appearance. + pub vertical_rail: Rail, + /// The horizontal [`Rail`] appearance. + pub horizontal_rail: Rail, /// The [`Background`] of the gap between a horizontal and vertical scrollbar. pub gap: Option<Background>, } /// The appearance of the scrollbar of a scrollable. #[derive(Debug, Clone, Copy)] -pub struct Scrollbar { +pub struct Rail { /// The [`Background`] of a scrollbar. pub background: Option<Background>, /// The [`Border`] of a scrollbar. @@ -1659,7 +1744,7 @@ impl Catalog for Theme { pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let scrollbar = Scrollbar { + let scrollbar = Rail { background: Some(palette.background.weak.color.into()), border: Border::rounded(2), scroller: Scroller { @@ -1671,15 +1756,15 @@ pub fn default(theme: &Theme, status: Status) -> Style { match status { Status::Active => Style { container: container::Style::default(), - vertical_scrollbar: scrollbar, - horizontal_scrollbar: scrollbar, + vertical_rail: scrollbar, + horizontal_rail: scrollbar, gap: None, }, Status::Hovered { is_horizontal_scrollbar_hovered, is_vertical_scrollbar_hovered, } => { - let hovered_scrollbar = Scrollbar { + let hovered_scrollbar = Rail { scroller: Scroller { color: palette.primary.strong.color, ..scrollbar.scroller @@ -1689,12 +1774,12 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { container: container::Style::default(), - vertical_scrollbar: if is_vertical_scrollbar_hovered { + vertical_rail: if is_vertical_scrollbar_hovered { hovered_scrollbar } else { scrollbar }, - horizontal_scrollbar: if is_horizontal_scrollbar_hovered { + horizontal_rail: if is_horizontal_scrollbar_hovered { hovered_scrollbar } else { scrollbar @@ -1706,7 +1791,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { is_horizontal_scrollbar_dragged, is_vertical_scrollbar_dragged, } => { - let dragged_scrollbar = Scrollbar { + let dragged_scrollbar = Rail { scroller: Scroller { color: palette.primary.base.color, ..scrollbar.scroller @@ -1716,12 +1801,12 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { container: container::Style::default(), - vertical_scrollbar: if is_vertical_scrollbar_dragged { + vertical_rail: if is_vertical_scrollbar_dragged { dragged_scrollbar } else { scrollbar }, - horizontal_scrollbar: if is_horizontal_scrollbar_dragged { + horizontal_rail: if is_horizontal_scrollbar_dragged { dragged_scrollbar } else { scrollbar From 8e9099cdd30fb8a830340889f0e89eb2693e1d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 08:11:19 +0200 Subject: [PATCH 092/657] Fix broken doc links in `widget::scrollable` --- widget/src/scrollable.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 35613910..e6208528 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -197,7 +197,7 @@ pub enum Direction { } impl Direction { - /// Returns the [`Properties`] of the horizontal scrollbar, if any. + /// Returns the horizontal [`Scrollbar`], if any. pub fn horizontal(&self) -> Option<&Scrollbar> { match self { Self::Horizontal(properties) => Some(properties), @@ -206,7 +206,7 @@ impl Direction { } } - /// Returns the [`Properties`] of the vertical scrollbar, if any. + /// Returns the vertical [`Scrollbar`], if any. pub fn vertical(&self) -> Option<&Scrollbar> { match self { Self::Vertical(properties) => Some(properties), @@ -222,7 +222,7 @@ impl Default for Direction { } } -/// Properties of a scrollbar within a [`Scrollable`]. +/// A scrollbar within a [`Scrollable`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Scrollbar { width: f32, @@ -245,7 +245,7 @@ impl Default for Scrollbar { } impl Scrollbar { - /// Creates new [`Properties`] for use in a [`Scrollable`]. + /// Creates new [`Scrollbar`] for use in a [`Scrollable`]. pub fn new() -> Self { Self::default() } From 8c110c1be92b8ede654aa605813e758fc5195d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 10:21:45 +0200 Subject: [PATCH 093/657] Make window visible after surface creation in `iced_winit` --- winit/src/program.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 3a4e2e48..1590cd3c 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -494,6 +494,8 @@ where let exit_on_close_request = settings.exit_on_close_request; + let visible = settings.visible; + #[cfg(target_arch = "wasm32")] let target = settings.platform_specific.target.clone(); @@ -507,7 +509,8 @@ where .or(event_loop .primary_monitor()), self.id.clone(), - ), + ) + .with_visible(false), ) .expect("Create window"); @@ -563,6 +566,7 @@ where id, window, exit_on_close_request, + make_visible: visible, }, ); } @@ -612,6 +616,7 @@ enum Event<Message: 'static> { id: window::Id, window: winit::window::Window, exit_on_close_request: bool, + make_visible: bool, }, EventLoopAwakened(winit::event::Event<Message>), } @@ -667,6 +672,7 @@ async fn run_instance<P, C>( id, window, exit_on_close_request, + make_visible, } => { let window = window_manager.insert( id, @@ -691,6 +697,10 @@ async fn run_instance<P, C>( ); let _ = ui_caches.insert(id, user_interface::Cache::default()); + if make_visible { + window.raw.set_visible(true); + } + events.push(( id, core::Event::Window(window::Event::Opened { From 1c1bee6fd822db1e5fdf45953d3b3970a7e50510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 11 Jul 2024 10:44:44 +0200 Subject: [PATCH 094/657] Finish `window::open` only when window fully opens ... and run initial `Task` after `window::open` for applications. This fixes certain race conditions. --- winit/src/program.rs | 47 +++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 1590cd3c..e7d3294d 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -202,6 +202,16 @@ where }; let (program, task) = runtime.enter(|| P::new(flags)); + let is_daemon = window_settings.is_none(); + + let task = if let Some(window_settings) = window_settings { + let mut task = Some(task); + + runtime::window::open(window_settings) + .then(move |_| task.take().unwrap_or(Task::none())) + } else { + task + }; if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); @@ -223,6 +233,7 @@ where boot_receiver, event_receiver, control_sender, + is_daemon, )); let context = task::Context::from_waker(task::noop_waker_ref()); @@ -231,7 +242,7 @@ where instance: std::pin::Pin<Box<F>>, context: task::Context<'static>, id: Option<String>, - boot: Option<BootConfig<Message, C>>, + boot: Option<BootConfig<C>>, sender: mpsc::UnboundedSender<Event<Action<Message>>>, receiver: mpsc::UnboundedReceiver<Control>, error: Option<Error>, @@ -242,11 +253,9 @@ where queued_events: Vec<Event<Action<Message>>>, } - struct BootConfig<Message: 'static, C> { - proxy: Proxy<Message>, + struct BootConfig<C> { sender: oneshot::Sender<Boot<C>>, fonts: Vec<Cow<'static, [u8]>>, - window_settings: Option<window::Settings>, graphics_settings: graphics::Settings, } @@ -255,10 +264,8 @@ where context, id: settings.id, boot: Some(BootConfig { - proxy, sender: boot_sender, fonts: settings.fonts, - window_settings, graphics_settings, }), sender: event_sender, @@ -280,10 +287,8 @@ where { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let Some(BootConfig { - mut proxy, sender, fonts, - window_settings, graphics_settings, }) = self.boot.take() else { @@ -316,23 +321,10 @@ where compositor, clipboard, window: window.id(), - is_daemon: window_settings.is_none(), }) .ok() .expect("Send boot event"); - if let Some(window_settings) = window_settings { - let (sender, _receiver) = oneshot::channel(); - - proxy.send_action(Action::Window( - runtime::window::Action::Open( - window::Id::unique(), - window_settings, - sender, - ), - )); - } - Ok::<_, graphics::Error>(()) }; @@ -490,6 +482,7 @@ where settings, title, monitor, + on_open, } => { let exit_on_close_request = settings.exit_on_close_request; @@ -567,6 +560,7 @@ where window, exit_on_close_request, make_visible: visible, + on_open, }, ); } @@ -608,7 +602,6 @@ struct Boot<C> { compositor: C, clipboard: Clipboard, window: winit::window::WindowId, - is_daemon: bool, } enum Event<Message: 'static> { @@ -617,6 +610,7 @@ enum Event<Message: 'static> { window: winit::window::Window, exit_on_close_request: bool, make_visible: bool, + on_open: oneshot::Sender<window::Id>, }, EventLoopAwakened(winit::event::Event<Message>), } @@ -629,6 +623,7 @@ enum Control { settings: window::Settings, title: String, monitor: Option<winit::monitor::MonitorHandle>, + on_open: oneshot::Sender<window::Id>, }, } @@ -640,6 +635,7 @@ async fn run_instance<P, C>( mut boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, mut control_sender: mpsc::UnboundedSender<Control>, + is_daemon: bool, ) where P: Program + 'static, C: Compositor<Renderer = P::Renderer> + 'static, @@ -652,7 +648,6 @@ async fn run_instance<P, C>( mut compositor, mut clipboard, window: boot_window, - is_daemon, } = boot.try_recv().ok().flatten().expect("Receive boot"); let mut window_manager = WindowManager::new(); @@ -673,6 +668,7 @@ async fn run_instance<P, C>( window, exit_on_close_request, make_visible, + on_open, } => { let window = window_manager.insert( id, @@ -708,6 +704,8 @@ async fn run_instance<P, C>( size: window.size(), }), )); + + let _ = on_open.send(id); } Event::EventLoopAwakened(event) => { match event { @@ -1180,10 +1178,9 @@ fn run_action<P, C>( settings, title: program.title(id), monitor, + on_open: channel, }) .expect("Send control action"); - - let _ = channel.send(id); } window::Action::Close(id) => { let _ = window_manager.remove(id); From 97e35f7d37ed857fba9b8d831c82b2cc8cf7b31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 12:08:35 +0200 Subject: [PATCH 095/657] Add `on_press_with` method for `Button` This allows using a closure to produce the message only when the `Button` is actually pressed. Useful when generating the message may be expensive. --- widget/src/button.rs | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/widget/src/button.rs b/widget/src/button.rs index 5d446fea..fb505d6e 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -52,7 +52,7 @@ where Theme: Catalog, { content: Element<'a, Message, Theme, Renderer>, - on_press: Option<Message>, + on_press: Option<OnPress<'a, Message>>, width: Length, height: Length, padding: Padding, @@ -60,6 +60,20 @@ where class: Theme::Class<'a>, } +enum OnPress<'a, Message> { + Direct(Message), + Closure(Box<dyn Fn() -> Message + 'a>), +} + +impl<'a, Message: Clone> OnPress<'a, Message> { + fn get(&self) -> Message { + match self { + OnPress::Direct(message) => message.clone(), + OnPress::Closure(f) => f(), + } + } +} + impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where Renderer: crate::core::Renderer, @@ -105,7 +119,23 @@ where /// /// Unless `on_press` is called, the [`Button`] will be disabled. pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); + self.on_press = Some(OnPress::Direct(on_press)); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// This is analogous to [`Button::on_press`], but using a closure to produce + /// the message. + /// + /// This closure will only be called when the [`Button`] is actually pressed and, + /// therefore, this method is useful to reduce overhead if creating the resulting + /// message is slow. + pub fn on_press_with( + mut self, + on_press: impl Fn() -> Message + 'a, + ) -> Self { + self.on_press = Some(OnPress::Closure(Box::new(on_press))); self } @@ -114,7 +144,7 @@ where /// /// If `None`, the [`Button`] will be disabled. pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { - self.on_press = on_press; + self.on_press = on_press.map(OnPress::Direct); self } @@ -258,7 +288,8 @@ where } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.clone() { + if let Some(on_press) = self.on_press.as_ref().map(OnPress::get) + { let state = tree.state.downcast_mut::<State>(); if state.is_pressed { From c1c978972097c8c43135647fe476b08e9bd691e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 13:02:19 +0200 Subject: [PATCH 096/657] Add `abort_on_drop` to `task::Handle` You may not want to worry about aborting tasks manually. --- runtime/src/task.rs | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index b75aca89..72f408e0 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -168,9 +168,21 @@ impl<T> Task<T> { Some(stream) => { let (stream, handle) = stream::abortable(stream); - (Self(Some(boxed_stream(stream))), Handle(Some(handle))) + ( + Self(Some(boxed_stream(stream))), + Handle { + raw: Some(handle), + abort_on_drop: false, + }, + ) } - None => (Self(None), Handle(None)), + None => ( + Self(None), + Handle { + raw: None, + abort_on_drop: false, + }, + ), } } @@ -195,19 +207,34 @@ impl<T> Task<T> { /// A handle to a [`Task`] that can be used for aborting it. #[derive(Debug, Clone)] -pub struct Handle(Option<stream::AbortHandle>); +pub struct Handle { + raw: Option<stream::AbortHandle>, + abort_on_drop: bool, +} impl Handle { /// Aborts the [`Task`] of this [`Handle`]. pub fn abort(&self) { - if let Some(handle) = &self.0 { + if let Some(handle) = &self.raw { handle.abort(); } } + /// Returns a new [`Handle`] that will call [`Handle::abort`] whenever + /// it is dropped. + /// + /// This can be really useful if you do not want to worry about calling + /// [`Handle::abort`] yourself. + pub fn abort_on_drop(mut self) -> Self { + Self { + raw: self.raw.take(), + abort_on_drop: true, + } + } + /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted. pub fn is_aborted(&self) -> bool { - if let Some(handle) = &self.0 { + if let Some(handle) = &self.raw { handle.is_aborted() } else { true @@ -215,6 +242,14 @@ impl Handle { } } +impl Drop for Handle { + fn drop(&mut self) { + if self.abort_on_drop { + self.abort(); + } + } +} + impl<T> Task<Option<T>> { /// Executes a new [`Task`] after this one, only when it produces `Some` value. /// From f9dd5cbb099bbe44a57b6369be54a442363b7a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 15:11:30 +0200 Subject: [PATCH 097/657] Introduce helper methods for alignment for all widgets --- core/src/alignment.rs | 45 ++++++++++++++ core/src/pixels.rs | 26 +++++++- core/src/widget/text.rs | 47 +++++++++++++-- examples/bezier_tool/src/main.rs | 3 +- examples/color_palette/src/main.rs | 4 +- examples/combo_box/src/main.rs | 4 +- examples/component/src/main.rs | 6 +- examples/counter/src/main.rs | 3 +- examples/custom_quad/src/main.rs | 4 +- examples/custom_shader/src/main.rs | 6 +- examples/custom_widget/src/main.rs | 4 +- examples/download_progress/src/main.rs | 10 ++-- examples/editor/src/main.rs | 4 +- examples/events/src/main.rs | 17 ++---- examples/exit/src/main.rs | 4 +- examples/ferris/src/main.rs | 12 ++-- examples/game_of_life/src/main.rs | 6 +- examples/gradient/src/main.rs | 6 +- examples/integration/src/controls.rs | 3 +- examples/layout/src/main.rs | 12 ++-- examples/loading_spinners/src/main.rs | 4 +- examples/loupe/src/main.rs | 4 +- examples/modal/src/main.rs | 5 +- examples/multi_window/src/main.rs | 4 +- examples/pane_grid/src/main.rs | 16 ++--- examples/pick_list/src/main.rs | 4 +- examples/pokedex/src/main.rs | 10 ++-- examples/qr_code/src/main.rs | 6 +- examples/screenshot/src/main.rs | 36 ++++-------- examples/scrollable/src/main.rs | 32 +++++----- examples/sierpinski_triangle/src/main.rs | 2 +- examples/slider/src/main.rs | 4 +- examples/stopwatch/src/main.rs | 16 ++--- examples/styling/src/main.rs | 8 +-- examples/toast/src/main.rs | 6 +- examples/todos/src/main.rs | 27 +++------ examples/tour/src/main.rs | 30 +++------- examples/vectorial_text/src/main.rs | 4 +- examples/visible_bounds/src/main.rs | 5 +- examples/websocket/src/main.rs | 13 +--- widget/src/column.rs | 30 +++++++--- widget/src/container.rs | 61 ++++++++++++------- widget/src/helpers.rs | 4 +- widget/src/row.rs | 30 +++++++--- widget/src/scrollable.rs | 75 +++++++++++++++--------- 45 files changed, 380 insertions(+), 282 deletions(-) diff --git a/core/src/alignment.rs b/core/src/alignment.rs index 51b7fca9..cacf7ce3 100644 --- a/core/src/alignment.rs +++ b/core/src/alignment.rs @@ -1,5 +1,30 @@ //! Align and position widgets. +/// Returns a value representing center alignment. +pub const fn center() -> Alignment { + Alignment::Center +} + +/// Returns a value representing left alignment. +pub const fn left() -> Horizontal { + Horizontal::Left +} + +/// Returns a value representing right alignment. +pub const fn right() -> Horizontal { + Horizontal::Right +} + +/// Returns a value representing top alignment. +pub const fn top() -> Vertical { + Vertical::Top +} + +/// Returns a value representing bottom alignment. +pub const fn bottom() -> Vertical { + Vertical::Bottom +} + /// Alignment on the axis of a container. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Alignment { @@ -46,6 +71,16 @@ pub enum Horizontal { Right, } +impl From<Alignment> for Horizontal { + fn from(alignment: Alignment) -> Self { + match alignment { + Alignment::Start => Self::Left, + Alignment::Center => Self::Center, + Alignment::End => Self::Right, + } + } +} + /// The vertical [`Alignment`] of some resource. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Vertical { @@ -58,3 +93,13 @@ pub enum Vertical { /// Align bottom Bottom, } + +impl From<Alignment> for Vertical { + fn from(alignment: Alignment) -> Self { + match alignment { + Alignment::Start => Self::Top, + Alignment::Center => Self::Center, + Alignment::End => Self::Bottom, + } + } +} diff --git a/core/src/pixels.rs b/core/src/pixels.rs index 425c0028..f5550a10 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -6,7 +6,7 @@ /// (e.g. `impl Into<Pixels>`) and, since `Pixels` implements `From` both for /// `f32` and `u16`, you should be able to provide both integers and float /// literals as needed. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] pub struct Pixels(pub f32); impl From<f32> for Pixels { @@ -27,6 +27,30 @@ impl From<Pixels> for f32 { } } +impl std::ops::Add for Pixels { + type Output = Pixels; + + fn add(self, rhs: Self) -> Self { + Pixels(self.0 + rhs.0) + } +} + +impl std::ops::Add<f32> for Pixels { + type Output = Pixels; + + fn add(self, rhs: f32) -> Self { + Pixels(self.0 + rhs) + } +} + +impl std::ops::Mul for Pixels { + type Output = Pixels; + + fn mul(self, rhs: Self) -> Self { + Pixels(self.0 * rhs.0) + } +} + impl std::ops::Mul<f32> for Pixels { type Output = Pixels; diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 081407e5..6ae95c8b 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -86,21 +86,56 @@ where self } + /// Centers the [`Text`], both horizontally and vertically. + pub fn center(self) -> Self { + self.center_x().center_y() + } + + /// Centers the [`Text`] horizontally. + pub fn center_x(self) -> Self { + self.align_x(alignment::center()) + } + + /// Aligns the [`Text`] to the left, the default. + pub fn align_left(self) -> Self { + self.align_x(alignment::left()) + } + + /// Aligns the [`Text`] to the right. + pub fn align_right(self) -> Self { + self.align_x(alignment::right()) + } + + /// Centers the [`Text`] vertically. + pub fn center_y(self) -> Self { + self.align_y(alignment::center()) + } + + /// Aligns the [`Text`] to the top, the default. + pub fn align_top(self) -> Self { + self.align_y(alignment::top()) + } + + /// Aligns the [`Text`] to the bottom. + pub fn align_bottom(self) -> Self { + self.align_y(alignment::bottom()) + } + /// Sets the [`alignment::Horizontal`] of the [`Text`]. - pub fn horizontal_alignment( + pub fn align_x( mut self, - alignment: alignment::Horizontal, + alignment: impl Into<alignment::Horizontal>, ) -> Self { - self.horizontal_alignment = alignment; + self.horizontal_alignment = alignment.into(); self } /// Sets the [`alignment::Vertical`] of the [`Text`]. - pub fn vertical_alignment( + pub fn align_y( mut self, - alignment: alignment::Vertical, + alignment: impl Into<alignment::Vertical>, ) -> Self { - self.vertical_alignment = alignment; + self.vertical_alignment = alignment.into(); self } diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index eaf84b97..2e5490e2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,5 +1,4 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::alignment; use iced::widget::{button, container, horizontal_space, hover}; use iced::{Element, Length, Theme}; @@ -49,7 +48,7 @@ impl Example { ) .padding(10) .width(Length::Fill) - .align_x(alignment::Horizontal::Right) + .align_right() }, )) .padding(20) diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index e4b19731..870e4ca7 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,4 +1,4 @@ -use iced::alignment::{self, Alignment}; +use iced::alignment; use iced::mouse; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; @@ -320,7 +320,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> { text(color.to_string()).width(185).size(12), ] .spacing(10) - .align_items(Alignment::Center) + .center_y() .into() } } diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index ff759ab4..0b321472 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,7 +1,7 @@ use iced::widget::{ center, column, combo_box, scrollable, text, vertical_space, }; -use iced::{Alignment, Element, Length}; +use iced::{Element, Length}; pub fn main() -> iced::Result { iced::run("Combo Box - Iced", Example::update, Example::view) @@ -65,7 +65,7 @@ impl Example { vertical_space().height(150), ] .width(Length::Fill) - .align_items(Alignment::Center) + .center_x() .spacing(10); center(scrollable(content)).into() diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 5625f12a..14d3d9ba 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -34,7 +34,6 @@ impl Component { } mod numeric_input { - use iced::alignment::{self, Alignment}; use iced::widget::{button, component, row, text, text_input, Component}; use iced::{Element, Length, Size}; @@ -108,8 +107,7 @@ mod numeric_input { text(label) .width(Length::Fill) .height(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Center), + .center(), ) .width(40) .height(40) @@ -130,7 +128,7 @@ mod numeric_input { .padding(10), button("+", Event::IncrementPressed), ] - .align_items(Alignment::Center) + .center_y() .spacing(10) .into() } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 0dd7a976..848c4c6c 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,5 +1,4 @@ use iced::widget::{button, column, text, Column}; -use iced::Alignment; pub fn main() -> iced::Result { iced::run("A cool counter", Counter::update, Counter::view) @@ -35,6 +34,6 @@ impl Counter { button("Decrement").on_press(Message::Decrement) ] .padding(20) - .align_items(Alignment::Center) + .center_x() } } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index b53a40d6..ce7eb4ac 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -82,7 +82,7 @@ mod quad { } use iced::widget::{center, column, slider, text}; -use iced::{Alignment, Color, Element, Shadow, Vector}; +use iced::{Color, Element, Shadow, Vector}; pub fn main() -> iced::Result { iced::run("Custom Quad - Iced", Example::update, Example::view) @@ -185,7 +185,7 @@ impl Example { .padding(20) .spacing(20) .max_width(500) - .align_items(Alignment::Center); + .center_x(); center(content).into() } diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index b04a8183..3e29e7be 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -6,7 +6,7 @@ use iced::time::Instant; use iced::widget::shader::wgpu; use iced::widget::{center, checkbox, column, row, shader, slider, text}; use iced::window; -use iced::{Alignment, Color, Element, Length, Subscription}; +use iced::{Color, Element, Length, Subscription}; fn main() -> iced::Result { iced::application( @@ -122,12 +122,12 @@ impl IcedCubes { let controls = column![top_controls, bottom_controls,] .spacing(10) .padding(20) - .align_items(Alignment::Center); + .center_x(); let shader = shader(&self.scene).width(Length::Fill).height(Length::Fill); - center(column![shader, controls].align_items(Alignment::Center)).into() + center(column![shader, controls].center_x()).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 3cf10e22..9f5dcfd0 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -83,7 +83,7 @@ mod circle { use circle::circle; use iced::widget::{center, column, slider, text}; -use iced::{Alignment, Element}; +use iced::Element; pub fn main() -> iced::Result { iced::run("Custom Widget - Iced", Example::update, Example::view) @@ -120,7 +120,7 @@ impl Example { .padding(20) .spacing(20) .max_width(500) - .align_items(Alignment::Center); + .center_x(); center(content).into() } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index d91e5eab..8064b4ae 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,7 +1,7 @@ mod download; use iced::widget::{button, center, column, progress_bar, text, Column}; -use iced::{Alignment, Element, Subscription}; +use iced::{Element, Subscription}; pub fn main() -> iced::Result { iced::application( @@ -69,7 +69,7 @@ impl Example { .padding(10), ) .spacing(20) - .align_items(Alignment::End); + .align_right(); center(downloads).padding(20).into() } @@ -160,7 +160,7 @@ impl Download { State::Finished => { column!["Download finished!", button("Start again")] .spacing(10) - .align_items(Alignment::Center) + .center_x() .into() } State::Downloading { .. } => { @@ -171,14 +171,14 @@ impl Download { button("Try again").on_press(Message::Download(self.id)), ] .spacing(10) - .align_items(Alignment::Center) + .center_x() .into(), }; Column::new() .spacing(10) .padding(10) - .align_items(Alignment::Center) + .center_x() .push(progress_bar) .push(control) .into() diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index ce3c478d..e24c4ab6 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme}; +use iced::{Element, Font, Length, Subscription, Task, Theme}; use std::ffi; use std::io; @@ -158,7 +158,7 @@ impl Editor { .padding([5, 10]) ] .spacing(10) - .align_items(Alignment::Center); + .center_y(); let status = row![ text(if let Some(path) = &self.file { diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 2cd3c5d8..7c9e78a6 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,8 +1,7 @@ -use iced::alignment; use iced::event::{self, Event}; use iced::widget::{button, center, checkbox, text, Column}; use iced::window; -use iced::{Alignment, Element, Length, Subscription, Task}; +use iced::{Element, Length, Subscription, Task}; pub fn main() -> iced::Result { iced::application("Events - Iced", Events::update, Events::view) @@ -67,17 +66,13 @@ impl Events { let toggle = checkbox("Listen to runtime events", self.enabled) .on_toggle(Message::Toggled); - let exit = button( - text("Exit") - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ) - .width(100) - .padding(10) - .on_press(Message::Exit); + let exit = button(text("Exit").width(Length::Fill).center_x()) + .width(100) + .padding(10) + .on_press(Message::Exit); let content = Column::new() - .align_items(Alignment::Center) + .center_x() .spacing(20) .push(events) .push(toggle) diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 1f108df2..03ddfb2c 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{button, center, column}; use iced::window; -use iced::{Alignment, Element, Task}; +use iced::{Element, Task}; pub fn main() -> iced::Result { iced::application("Exit - Iced", Exit::update, Exit::view).run() @@ -44,7 +44,7 @@ impl Exit { ] } .spacing(10) - .align_items(Alignment::Center); + .center_x(); center(content).padding(20).into() } diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 88006898..5d468de1 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation, + Color, ContentFit, Degrees, Element, Length, Radians, Rotation, Subscription, Theme, }; @@ -108,7 +108,7 @@ impl Image { "I am Ferris!" ] .spacing(20) - .align_items(Alignment::Center); + .center_x(); let fit = row![ pick_list( @@ -134,7 +134,7 @@ impl Image { .width(Length::Fill), ] .spacing(10) - .align_items(Alignment::End); + .align_bottom(); let properties = row![ with_value( @@ -159,12 +159,12 @@ impl Image { .size(12) ] .spacing(10) - .align_items(Alignment::Center), + .center_y(), format!("Rotation: {:.0}°", f32::from(self.rotation.degrees())) ) ] .spacing(10) - .align_items(Alignment::End); + .align_bottom(); container(column![fit, center(i_am_ferris), properties].spacing(10)) .padding(10) @@ -206,6 +206,6 @@ fn with_value<'a>( ) -> Element<'a, Message> { column![control.into(), text(value).size(12).line_height(1.0)] .spacing(2) - .align_items(Alignment::Center) + .center_x() .into() } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 421f862a..38eb692b 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -9,7 +9,7 @@ use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, }; -use iced::{Alignment, Element, Length, Subscription, Task, Theme}; +use iced::{Element, Length, Subscription, Task, Theme}; use std::time::Duration; pub fn main() -> iced::Result { @@ -169,7 +169,7 @@ fn view_controls<'a>( slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), text!("x{speed}").size(16), ] - .align_items(Alignment::Center) + .center_y() .spacing(10); row![ @@ -186,7 +186,7 @@ fn view_controls<'a>( ] .padding(10) .spacing(20) - .align_items(Alignment::Center) + .center_y() .into() } diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index e5b19443..018bab82 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -3,7 +3,7 @@ use iced::gradient; use iced::widget::{ checkbox, column, container, horizontal_space, row, slider, text, }; -use iced::{Alignment, Color, Element, Length, Radians, Theme}; +use iced::{Color, Element, Length, Radians, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); @@ -77,7 +77,7 @@ impl Gradient { ] .spacing(8) .padding(8) - .align_items(Alignment::Center); + .center_y(); let transparency_toggle = iced::widget::Container::new( checkbox("Transparent window", transparent) @@ -129,6 +129,6 @@ fn color_picker(label: &str, color: Color) -> Element<'_, Color> { ] .spacing(8) .padding(8) - .align_items(Alignment::Center) + .center_y() .into() } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index d0654996..b03aa6d4 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,6 +1,5 @@ use iced_wgpu::Renderer; use iced_widget::{column, container, row, slider, text, text_input}; -use iced_winit::core::alignment; use iced_winit::core::{Color, Element, Length, Theme}; use iced_winit::runtime::{Program, Task}; @@ -87,7 +86,7 @@ impl Program for Controls { ) .padding(10) .height(Length::Fill) - .align_y(alignment::Vertical::Bottom) + .align_bottom() .into() } } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 2e774415..2bc7fb30 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ pick_list, row, scrollable, text, }; use iced::{ - color, Alignment, Element, Font, Length, Point, Rectangle, Renderer, - Subscription, Theme, + color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, + Theme, }; pub fn main() -> iced::Result { @@ -74,7 +74,7 @@ impl Layout { pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected), ] .spacing(20) - .align_items(Alignment::Center); + .center_y(); let example = center(if self.explain { self.example.view().explain(color!(0x0000ff)) @@ -234,7 +234,7 @@ fn application<'a>() -> Element<'a, Message> { square(40), ] .padding(10) - .align_items(Alignment::Center), + .center_y(), ) .style(|theme| { let palette = theme.extended_palette(); @@ -248,7 +248,7 @@ fn application<'a>() -> Element<'a, Message> { .spacing(40) .padding(10) .width(200) - .align_items(Alignment::Center), + .center_x(), ) .style(container::rounded_box) .center_y(Length::Fill); @@ -263,7 +263,7 @@ fn application<'a>() -> Element<'a, Message> { "The end" ] .spacing(40) - .align_items(Alignment::Center) + .center_x() .width(Length::Fill), ) .height(Length::Fill), diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 503f2d7a..7fa7ac97 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -67,7 +67,7 @@ impl LoadingSpinners { Duration::from_secs_f32(self.cycle_duration) ) ] - .align_items(iced::Alignment::Center) + .center_y() .spacing(20.0), ) }) @@ -83,7 +83,7 @@ impl LoadingSpinners { .width(200.0), text!("{:.2}s", self.cycle_duration), ] - .align_items(iced::Alignment::Center) + .center_y() .spacing(20.0), ), ) diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index c4d3b449..8c9b4def 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{button, center, column, text}; -use iced::{Alignment, Element}; +use iced::Element; use loupe::loupe; @@ -39,7 +39,7 @@ impl Loupe { button("Decrement").on_press(Message::Decrement) ] .padding(20) - .align_items(Alignment::Center), + .center_x(), )) .into() } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 413485e7..6f0f7182 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ self, button, center, column, container, horizontal_space, mouse_area, opaque, pick_list, row, stack, text, text_input, }; -use iced::{Alignment, Color, Element, Length, Subscription, Task}; +use iced::{Color, Element, Length, Subscription, Task}; use std::fmt; @@ -96,7 +96,6 @@ impl App { let content = container( column![ row![text("Top Left"), horizontal_space(), text("Top Right")] - .align_items(Alignment::Start) .height(Length::Fill), center(button(text("Show Modal")).on_press(Message::ShowModal)), row![ @@ -104,7 +103,7 @@ impl App { horizontal_space(), text("Bottom Right") ] - .align_items(Alignment::End) + .align_bottom() .height(Length::Fill), ] .height(Length::Fill), diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 460ca3b5..b1276320 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ text_input, }; use iced::window; -use iced::{Alignment, Element, Length, Subscription, Task, Theme, Vector}; +use iced::{Element, Length, Subscription, Task, Theme, Vector}; use std::collections::BTreeMap; @@ -189,7 +189,7 @@ impl Window { column![scale_input, title_input, new_window_button] .spacing(50) .width(Length::Fill) - .align_items(Alignment::Center), + .center_x(), ); container(content).center_x(200).into() diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index db9f7a05..1d1efeeb 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,4 +1,3 @@ -use iced::alignment::{self, Alignment}; use iced::keyboard; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{ @@ -255,15 +254,10 @@ fn view_content<'a>( size: Size, ) -> Element<'a, Message> { let button = |label, message| { - button( - text(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) + button(text(label).width(Length::Fill).center_x().size(16)) + .width(Length::Fill) + .padding(8) + .on_press(message) }; let controls = column![ @@ -287,7 +281,7 @@ fn view_content<'a>( let content = column![text!("{}x{}", size.width, size.height).size(24), controls,] .spacing(10) - .align_items(Alignment::Center); + .center_x(); container(scrollable(content)) .center_y(Length::Fill) diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 2be6f5b0..038204f0 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{column, pick_list, scrollable, vertical_space}; -use iced::{Alignment, Element, Length}; +use iced::{Element, Length}; pub fn main() -> iced::Result { iced::run("Pick List - Iced", Example::update, Example::view) @@ -39,7 +39,7 @@ impl Example { vertical_space().height(600), ] .width(Length::Fill) - .align_items(Alignment::Center) + .center_x() .spacing(10); scrollable(content).into() diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 7414ae54..8131bb7e 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,6 @@ use iced::futures; use iced::widget::{self, center, column, image, row, text}; -use iced::{Alignment, Element, Length, Task}; +use iced::{Element, Length, Task}; pub fn main() -> iced::Result { iced::application(Pokedex::title, Pokedex::update, Pokedex::view) @@ -74,13 +74,13 @@ impl Pokedex { ] .max_width(500) .spacing(20) - .align_items(Alignment::End), + .align_right(), Pokedex::Errored => column![ text("Whoops! Something went wrong...").size(40), button("Try again").on_press(Message::Search) ] .spacing(20) - .align_items(Alignment::End), + .align_right(), }; center(content).into() @@ -106,14 +106,14 @@ impl Pokemon { text(&self.name).size(30).width(Length::Fill), text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]), ] - .align_items(Alignment::Center) + .center_y() .spacing(20), self.description.as_ref(), ] .spacing(20), ] .spacing(20) - .align_items(Alignment::Center) + .center_y() .into() } diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index b30ecf15..14db776e 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; -use iced::{Alignment, Element, Theme}; +use iced::{Element, Theme}; pub fn main() -> iced::Result { iced::application( @@ -58,7 +58,7 @@ impl QRGenerator { pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,) ] .spacing(10) - .align_items(Alignment::Center); + .center_y(); let content = column![title, input, choose_theme] .push_maybe( @@ -68,7 +68,7 @@ impl QRGenerator { ) .width(700) .spacing(20) - .align_items(Alignment::Center); + .center_x(); center(content).padding(20).into() } diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index acde8367..3b583d13 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,11 +1,8 @@ -use iced::alignment; use iced::keyboard; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; -use iced::{ - Alignment, ContentFit, Element, Length, Rectangle, Subscription, Task, -}; +use iced::{ContentFit, Element, Length, Rectangle, Subscription, Task}; use ::image as img; use ::image::ColorType; @@ -127,32 +124,24 @@ impl Example { .style(container::rounded_box); let crop_origin_controls = row![ - text("X:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("X:").width(30), numeric_input("0", self.x_input_value).map(Message::XInputChanged), - text("Y:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("Y:").width(30), numeric_input("0", self.y_input_value).map(Message::YInputChanged) ] .spacing(10) - .align_items(Alignment::Center); + .center_y(); let crop_dimension_controls = row![ - text("W:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("W:").width(30), numeric_input("0", self.width_input_value) .map(Message::WidthInputChanged), - text("H:") - .vertical_alignment(alignment::Vertical::Center) - .width(30), + text("H:").width(30), numeric_input("0", self.height_input_value) .map(Message::HeightInputChanged) ] .spacing(10) - .align_items(Alignment::Center); + .center_y(); let crop_controls = column![crop_origin_controls, crop_dimension_controls] @@ -162,7 +151,7 @@ impl Example { .map(|error| text!("Crop error! \n{error}")), ) .spacing(10) - .align_items(Alignment::Center); + .center_x(); let controls = { let save_result = @@ -203,7 +192,7 @@ impl Example { .width(Length::Fill), ] .spacing(10) - .align_items(Alignment::Center), + .center_x(), ] .push_maybe(save_result.map(text)) .spacing(40) @@ -215,7 +204,7 @@ impl Example { .spacing(10) .width(Length::Fill) .height(Length::Fill) - .align_items(Alignment::Center); + .center_y(); container(content).padding(10).into() } @@ -276,8 +265,5 @@ fn numeric_input( } fn centered_text(content: &str) -> Element<'_, Message> { - text(content) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .into() + text(content).width(Length::Fill).center_x().into() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 067dcd70..bf8fdedf 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; -use iced::{Alignment, Border, Color, Element, Length, Task, Theme}; +use iced::{Border, Color, Element, Length, Task, Theme}; use once_cell::sync::Lazy; @@ -24,7 +24,7 @@ struct ScrollableDemo { scrollbar_margin: u16, scroller_width: u16, current_scroll_offset: scrollable::RelativeOffset, - alignment: scrollable::Alignment, + anchor: scrollable::Anchor, } #[derive(Debug, Clone, Eq, PartialEq, Copy)] @@ -37,7 +37,7 @@ enum Direction { #[derive(Debug, Clone)] enum Message { SwitchDirection(Direction), - AlignmentChanged(scrollable::Alignment), + AlignmentChanged(scrollable::Anchor), ScrollbarWidthChanged(u16), ScrollbarMarginChanged(u16), ScrollerWidthChanged(u16), @@ -54,7 +54,7 @@ impl ScrollableDemo { scrollbar_margin: 0, scroller_width: 10, current_scroll_offset: scrollable::RelativeOffset::START, - alignment: scrollable::Alignment::Start, + anchor: scrollable::Anchor::Start, } } @@ -71,7 +71,7 @@ impl ScrollableDemo { } Message::AlignmentChanged(alignment) => { self.current_scroll_offset = scrollable::RelativeOffset::START; - self.alignment = alignment; + self.anchor = alignment; scrollable::snap_to( SCROLLABLE_ID.clone(), @@ -168,14 +168,14 @@ impl ScrollableDemo { text("Scrollable alignment:"), radio( "Start", - scrollable::Alignment::Start, - Some(self.alignment), + scrollable::Anchor::Start, + Some(self.anchor), Message::AlignmentChanged, ), radio( "End", - scrollable::Alignment::End, - Some(self.alignment), + scrollable::Anchor::End, + Some(self.anchor), Message::AlignmentChanged, ) ] @@ -212,7 +212,7 @@ impl ScrollableDemo { text("End!"), scroll_to_beginning_button(), ] - .align_items(Alignment::Center) + .center_x() .padding([40, 0, 40, 0]) .spacing(40), ) @@ -221,7 +221,7 @@ impl ScrollableDemo { .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) - .alignment(self.alignment), + .anchor(self.anchor), )) .width(Length::Fill) .height(Length::Fill) @@ -238,7 +238,7 @@ impl ScrollableDemo { scroll_to_beginning_button(), ] .height(450) - .align_items(Alignment::Center) + .center_y() .padding([0, 40, 0, 40]) .spacing(40), ) @@ -247,7 +247,7 @@ impl ScrollableDemo { .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) - .alignment(self.alignment), + .anchor(self.anchor), )) .width(Length::Fill) .height(Length::Fill) @@ -280,7 +280,7 @@ impl ScrollableDemo { text("Horizontal - End!"), scroll_to_beginning_button(), ] - .align_items(Alignment::Center) + .center_y() .padding([0, 40, 0, 40]) .spacing(40), ) @@ -289,7 +289,7 @@ impl ScrollableDemo { .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) - .alignment(self.alignment); + .anchor(self.anchor); scrollable::Direction::Both { horizontal: scrollbar, @@ -322,7 +322,7 @@ impl ScrollableDemo { let content: Element<Message> = column![scroll_controls, scrollable_content, progress_bars] - .align_items(Alignment::Center) + .center_x() .spacing(10) .into(); diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 4c751937..159b5597 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -60,7 +60,7 @@ impl SierpinskiEmulator { .padding(10) .spacing(20), ] - .align_items(iced::Alignment::Center) + .center_x() .into() } } diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index fd312763..5443c645 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{column, container, iced, slider, text, vertical_slider}; -use iced::{Alignment, Element, Length}; +use iced::{Element, Length}; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -46,7 +46,7 @@ impl Slider { column![v_slider, h_slider, text, iced(self.value as f32),] .width(Length::Fill) - .align_items(Alignment::Center) + .center_x() .spacing(20) .padding(20) .into() diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index bd56785a..ca7b8f91 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,8 +1,7 @@ -use iced::alignment; use iced::keyboard; use iced::time; use iced::widget::{button, center, column, row, text}; -use iced::{Alignment, Element, Subscription, Theme}; +use iced::{Element, Subscription, Theme}; use std::time::{Duration, Instant}; @@ -101,13 +100,8 @@ impl Stopwatch { ) .size(40); - let button = |label| { - button( - text(label).horizontal_alignment(alignment::Horizontal::Center), - ) - .padding(10) - .width(80) - }; + let button = + |label| button(text(label).center_x()).padding(10).width(80); let toggle_button = { let label = match self.state { @@ -124,9 +118,7 @@ impl Stopwatch { let controls = row![toggle_button, reset_button].spacing(20); - let content = column![duration, controls] - .align_items(Alignment::Center) - .spacing(20); + let content = column![duration, controls].center_x().spacing(20); center(content).into() } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 3124493b..8f979ea8 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ row, scrollable, slider, text, text_input, toggler, vertical_rule, vertical_space, }; -use iced::{Alignment, Element, Length, Theme}; +use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { iced::application("Styling - Iced", Styling::update, Styling::view) @@ -88,9 +88,7 @@ impl Styling { let content = column![ choose_theme, horizontal_rule(38), - row![text_input, button] - .spacing(10) - .align_items(Alignment::Center), + row![text_input, button].spacing(10).center_y(), slider, progress_bar, row![ @@ -100,7 +98,7 @@ impl Styling { ] .spacing(10) .height(100) - .align_items(Alignment::Center), + .center_y(), ] .spacing(20) .padding(20) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 232133b1..fd7f07c2 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -4,7 +4,7 @@ use iced::keyboard::key; use iced::widget::{ self, button, center, column, pick_list, row, slider, text, text_input, }; -use iced::{Alignment, Element, Length, Subscription, Task}; +use iced::{Element, Length, Subscription, Task}; use toast::{Status, Toast}; @@ -142,7 +142,7 @@ impl App { .spacing(5) .into() ), - column![add_toast].align_items(Alignment::End) + column![add_toast].center_x() ] .spacing(10) .max_width(200), @@ -245,7 +245,7 @@ mod toast { .on_press((on_close)(index)) .padding(3), ] - .align_items(Alignment::Center) + .center_y() ) .width(Length::Fill) .padding(5) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index b34f71ce..af651ee2 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,4 +1,3 @@ -use iced::alignment::{self, Alignment}; use iced::keyboard; use iced::widget::{ self, button, center, checkbox, column, container, keyed_column, row, @@ -196,7 +195,7 @@ impl Todos { .width(Length::Fill) .size(100) .color([0.5, 0.5, 0.5]) - .horizontal_alignment(alignment::Horizontal::Center); + .center_x(); let input = text_input("What needs to be done?", input_value) .id(INPUT_ID.clone()) @@ -355,7 +354,7 @@ impl Task { .style(button::text), ] .spacing(20) - .align_items(Alignment::Center) + .center_y() .into() } TaskState::Editing => { @@ -369,16 +368,14 @@ impl Task { row![ text_input, button( - row![delete_icon(), "Delete"] - .spacing(10) - .align_items(Alignment::Center) + row![delete_icon(), "Delete"].spacing(10).center_y() ) .on_press(TaskMessage::Delete) .padding(10) .style(button::danger) ] .spacing(20) - .align_items(Alignment::Center) + .center_y() .into() } } @@ -415,7 +412,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { .spacing(10) ] .spacing(20) - .align_items(Alignment::Center) + .center_y() .into() } @@ -440,12 +437,7 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - center( - text("Loading...") - .horizontal_alignment(alignment::Horizontal::Center) - .size(50), - ) - .into() + center(text("Loading...").center_x().size(50)).into() } fn empty_message(message: &str) -> Element<'_, Message> { @@ -453,7 +445,7 @@ fn empty_message(message: &str) -> Element<'_, Message> { text(message) .width(Length::Fill) .size(25) - .horizontal_alignment(alignment::Horizontal::Center) + .center_x() .color([0.7, 0.7, 0.7]), ) .height(200) @@ -464,10 +456,7 @@ fn empty_message(message: &str) -> Element<'_, Message> { const ICONS: Font = Font::with_name("Iced-Todos-Icons"); fn icon(unicode: char) -> Text<'static> { - text(unicode.to_string()) - .font(ICONS) - .width(20) - .horizontal_alignment(alignment::Horizontal::Center) + text(unicode.to_string()).font(ICONS).width(20).center_x() } fn edit_icon() -> Text<'static> { diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 94ba78ee..941c5b33 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,4 +1,3 @@ -use iced::alignment::{self, Alignment}; use iced::widget::{ button, checkbox, column, container, horizontal_space, image, radio, row, scrollable, slider, text, text_input, toggler, vertical_space, @@ -235,11 +234,7 @@ impl Tour { 0 to 100:", ) .push(slider(0..=100, self.slider, Message::SliderChanged)) - .push( - text(self.slider.to_string()) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ) + .push(text(self.slider.to_string()).width(Length::Fill).center_x()) } fn rows_and_columns(&self) -> Column<Message> { @@ -268,9 +263,7 @@ impl Tour { let spacing_section = column![ slider(0..=80, self.spacing, Message::SpacingChanged), - text!("{} px", self.spacing) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), + text!("{} px", self.spacing).width(Length::Fill).center_x(), ] .spacing(10); @@ -381,11 +374,7 @@ impl Tour { .push("An image that tries to keep its aspect ratio.") .push(ferris(width, filter_method)) .push(slider(100..=500, width, Message::ImageWidthChanged)) - .push( - text!("Width: {width} px") - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ) + .push(text!("Width: {width} px").width(Length::Fill).center_x()) .push( checkbox( "Use nearest interpolation", @@ -393,7 +382,7 @@ impl Tour { ) .on_toggle(Message::ImageUseNearestToggled), ) - .align_items(Alignment::Center) + .center_x() } fn scrollable(&self) -> Column<Message> { @@ -411,16 +400,11 @@ impl Tour { text("You are halfway there!") .width(Length::Fill) .size(30) - .horizontal_alignment(alignment::Horizontal::Center), + .center_x(), ) .push(vertical_space().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) - .push( - text("You made it!") - .width(Length::Fill) - .size(50) - .horizontal_alignment(alignment::Horizontal::Center), - ) + .push(text("You made it!").width(Length::Fill).size(50).center_x()) } fn text_input(&self) -> Column<Message> { @@ -465,7 +449,7 @@ impl Tour { value }) .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), + .center_x(), ) } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 6dd3273a..f7af486a 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -1,4 +1,4 @@ -use iced::alignment::{self, Alignment}; +use iced::alignment; use iced::mouse; use iced::widget::{ canvas, checkbox, column, horizontal_space, row, slider, text, @@ -85,7 +85,7 @@ impl VectorialText { ] .spacing(20), ] - .align_items(Alignment::Center) + .center_x() .spacing(10) ] .spacing(10) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index e46d1ff0..245600c5 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -5,8 +5,7 @@ use iced::widget::{ }; use iced::window; use iced::{ - Alignment, Color, Element, Font, Length, Point, Rectangle, Subscription, - Task, Theme, + Color, Element, Font, Length, Point, Rectangle, Subscription, Task, Theme, }; pub fn main() -> iced::Result { @@ -70,7 +69,7 @@ impl Example { .color_maybe(color), ] .height(40) - .align_items(Alignment::Center) + .center_y() }; let view_bounds = |label, bounds: Option<Rectangle>| { diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index d8246436..1bac61aa 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,6 +1,5 @@ mod echo; -use iced::alignment::{self, Alignment}; use iced::widget::{ self, button, center, column, row, scrollable, text, text_input, }; @@ -113,12 +112,8 @@ impl WebSocket { .on_input(Message::NewMessageChanged) .padding(10); - let mut button = button( - text("Send") - .height(40) - .vertical_alignment(alignment::Vertical::Center), - ) - .padding([0, 20]); + let mut button = + button(text("Send").height(40).center_y()).padding([0, 20]); if matches!(self.state, State::Connected(_)) { if let Some(message) = echo::Message::new(&self.new_message) { @@ -127,9 +122,7 @@ impl WebSocket { } } - row![input, button] - .spacing(10) - .align_items(Alignment::Center) + row![input, button].spacing(10).center_y() }; column![message_log, new_message_input] diff --git a/widget/src/column.rs b/widget/src/column.rs index 0b81c545..ef4ee99d 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,4 +1,5 @@ //! Distribute content vertically. +use crate::core::alignment::{self, Alignment}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -6,8 +7,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, + Size, Vector, Widget, }; /// A container that distributes its contents vertically. @@ -19,7 +20,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> width: Length, height: Length, max_width: f32, - align_items: Alignment, + align: Alignment, clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -63,7 +64,7 @@ where width: Length::Shrink, height: Length::Shrink, max_width: f32::INFINITY, - align_items: Alignment::Start, + align: Alignment::Start, clip: false, children, } @@ -103,9 +104,24 @@ where self } + /// Centers the contents of the [`Column`] horizontally. + pub fn center_x(self) -> Self { + self.align_x(Alignment::Center) + } + + /// Aligns the contents of the [`Column`] to the left. + pub fn align_left(self) -> Self { + self.align_x(alignment::left()) + } + + /// Aligns the contents of the [`Column`] to the right. + pub fn align_right(self) -> Self { + self.align_x(alignment::right()) + } + /// Sets the horizontal alignment of the contents of the [`Column`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; + pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self { + self.align = Alignment::from(align.into()); self } @@ -210,7 +226,7 @@ where self.height, self.padding, self.spacing, - self.align_items, + self.align, &self.children, &mut tree.children, ) diff --git a/widget/src/container.rs b/widget/src/container.rs index 08d5cb17..adfe347c 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -94,27 +94,19 @@ where /// Sets the [`Container`] to fill the available space in the horizontal axis. /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center_x`]. - /// /// Calling this method is equivalent to calling [`width`] with a /// [`Length::Fill`]. /// - /// [`center_x`]: Self::center_x /// [`width`]: Self::width pub fn fill_x(self) -> Self { self.width(Length::Fill) } - /// Sets the [`Container`] to fill the available space in the vetical axis. - /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center_y`]. + /// Sets the [`Container`] to fill the available space in the vertical axis. /// /// Calling this method is equivalent to calling [`height`] with a /// [`Length::Fill`]. /// - /// [`center_y`]: Self::center_x /// [`height`]: Self::height pub fn fill_y(self) -> Self { self.height(Length::Fill) @@ -125,7 +117,6 @@ where /// Calling this method is equivalent to chaining [`fill_x`] and /// [`fill_y`]. /// - /// [`center`]: Self::center /// [`fill_x`]: Self::fill_x /// [`fill_y`]: Self::fill_y pub fn fill(self) -> Self { @@ -144,18 +135,6 @@ where self } - /// Sets the content alignment for the horizontal axis of the [`Container`]. - pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { - self.horizontal_alignment = alignment; - self - } - - /// Sets the content alignment for the vertical axis of the [`Container`]. - pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { - self.vertical_alignment = alignment; - self - } - /// Sets the width of the [`Container`] and centers its contents horizontally. pub fn center_x(self, width: impl Into<Length>) -> Self { self.width(width).align_x(alignment::Horizontal::Center) @@ -179,6 +158,44 @@ where self.center_x(length).center_y(length) } + /// Aligns the contents of the [`Container`] to the left. + pub fn align_left(self) -> Self { + self.align_x(alignment::left()) + } + + /// Aligns the contents of the [`Container`] to the right. + pub fn align_right(self) -> Self { + self.align_x(alignment::right()) + } + + /// Aligns the contents of the [`Container`] to the top. + pub fn align_top(self) -> Self { + self.align_y(alignment::top()) + } + + /// Aligns the contents of the [`Container`] to the bottom. + pub fn align_bottom(self) -> Self { + self.align_y(alignment::bottom()) + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.horizontal_alignment = alignment.into(); + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.vertical_alignment = alignment.into(); + self + } + /// Sets whether the contents of the [`Container`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index d7631959..f27b7807 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -906,7 +906,7 @@ where + 'a, Theme: text::Catalog + crate::svg::Catalog + 'a, { - use crate::core::{Alignment, Font}; + use crate::core::Font; use crate::svg; use once_cell::sync::Lazy; @@ -921,7 +921,7 @@ where text("iced").size(text_size).font(Font::MONOSPACE) ] .spacing(text_size.0 / 3.0) - .align_items(Alignment::Center) + .center_y() .into() } diff --git a/widget/src/row.rs b/widget/src/row.rs index c8fcdb61..129feb7e 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -1,4 +1,5 @@ //! Distribute content horizontally. +use crate::core::alignment::{self, Alignment}; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -6,8 +7,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, - Size, Vector, Widget, + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, + Vector, Widget, }; /// A container that distributes its contents horizontally. @@ -17,7 +18,7 @@ pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { padding: Padding, width: Length, height: Length, - align_items: Alignment, + align: Alignment, clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -60,7 +61,7 @@ where padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, - align_items: Alignment::Start, + align: Alignment::Start, clip: false, children, } @@ -94,9 +95,24 @@ where self } + /// Centers the contents of the [`Row`] vertically. + pub fn center_y(self) -> Self { + self.align_y(Alignment::Center) + } + + /// Aligns the contents of the [`Row`] to the top. + pub fn align_top(self) -> Self { + self.align_y(alignment::top()) + } + + /// Aligns the contents of the [`Row`] to the bottom. + pub fn align_bottom(self) -> Self { + self.align_y(alignment::bottom()) + } + /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; + pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self { + self.align = Alignment::from(align.into()); self } @@ -199,7 +215,7 @@ where self.height, self.padding, self.spacing, - self.align_items, + self.align, &self.children, &mut tree.children, ) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e6208528..cd669cad 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -109,8 +109,28 @@ where self } - /// Sets the alignment of the horizontal direction of the [`Scrollable`], if applicable. - pub fn align_x(mut self, alignment: Alignment) -> Self { + /// Anchors the vertical [`Scrollable`] direction to the top. + pub fn anchor_top(self) -> Self { + self.anchor_y(Anchor::Start) + } + + /// Anchors the vertical [`Scrollable`] direction to the bottom. + pub fn anchor_bottom(self) -> Self { + self.anchor_y(Anchor::End) + } + + /// Anchors the horizontal [`Scrollable`] direction to the left. + pub fn anchor_left(self) -> Self { + self.anchor_x(Anchor::Start) + } + + /// Anchors the horizontal [`Scrollable`] direction to the right. + pub fn anchor_right(self) -> Self { + self.anchor_x(Anchor::End) + } + + /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable. + pub fn anchor_x(mut self, alignment: Anchor) -> Self { match &mut self.direction { Direction::Horizontal(horizontal) | Direction::Both { horizontal, .. } => { @@ -122,8 +142,8 @@ where self } - /// Sets the alignment of the vertical direction of the [`Scrollable`], if applicable. - pub fn align_y(mut self, alignment: Alignment) -> Self { + /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable. + pub fn anchor_y(mut self, alignment: Anchor) -> Self { match &mut self.direction { Direction::Vertical(vertical) | Direction::Both { vertical, .. } => { @@ -228,7 +248,7 @@ pub struct Scrollbar { width: f32, margin: f32, scroller_width: f32, - alignment: Alignment, + alignment: Anchor, embedded: bool, } @@ -238,7 +258,7 @@ impl Default for Scrollbar { width: 10.0, margin: 0.0, scroller_width: 10.0, - alignment: Alignment::Start, + alignment: Anchor::Start, embedded: false, } } @@ -250,26 +270,26 @@ impl Scrollbar { Self::default() } - /// Sets the scrollbar width of the [`Scrollable`] . + /// Sets the scrollbar width of the [`Scrollbar`] . pub fn width(mut self, width: impl Into<Pixels>) -> Self { self.width = width.into().0.max(0.0); self } - /// Sets the scrollbar margin of the [`Scrollable`] . + /// Sets the scrollbar margin of the [`Scrollbar`] . pub fn margin(mut self, margin: impl Into<Pixels>) -> Self { self.margin = margin.into().0; self } - /// Sets the scroller width of the [`Scrollable`] . + /// Sets the scroller width of the [`Scrollbar`] . pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { self.scroller_width = scroller_width.into().0.max(0.0); self } - /// Sets the alignment of the [`Scrollable`] . - pub fn alignment(mut self, alignment: Alignment) -> Self { + /// Sets the [`Anchor`] of the [`Scrollbar`] . + pub fn anchor(mut self, alignment: Anchor) -> Self { self.alignment = alignment; self } @@ -284,13 +304,14 @@ impl Scrollbar { } } -/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction. +/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`] +/// on a given axis. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum Alignment { - /// Content is aligned to the start of the [`Viewport`]. +pub enum Anchor { + /// Scroller is anchoer to the start of the [`Viewport`]. #[default] Start, - /// Content is aligned to the end of the [`Viewport`] + /// Content is aligned to the end of the [`Viewport`]. End, } @@ -1159,13 +1180,13 @@ impl Offset { self, viewport: f32, content: f32, - alignment: Alignment, + alignment: Anchor, ) -> f32 { let offset = self.absolute(viewport, content); match alignment { - Alignment::Start => offset, - Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + Anchor::Start => offset, + Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0), } } } @@ -1252,9 +1273,9 @@ impl State { .map(|p| p.alignment) .unwrap_or_default(); - let align = |alignment: Alignment, delta: f32| match alignment { - Alignment::Start => delta, - Alignment::End => -delta, + let align = |alignment: Anchor, delta: f32| match alignment { + Anchor::Start => delta, + Anchor::End => -delta, }; let delta = Vector::new( @@ -1592,14 +1613,14 @@ impl Scrollbars { pub(super) mod internals { use crate::core::{Point, Rectangle}; - use super::Alignment; + use super::Anchor; #[derive(Debug, Copy, Clone)] pub struct Scrollbar { pub total_bounds: Rectangle, pub bounds: Rectangle, pub scroller: Option<Scroller>, - pub alignment: Alignment, + pub alignment: Anchor, } impl Scrollbar { @@ -1621,8 +1642,8 @@ pub(super) mod internals { / (self.bounds.height - scroller.bounds.height); match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + Anchor::Start => percentage, + Anchor::End => 1.0 - percentage, } } else { 0.0 @@ -1642,8 +1663,8 @@ pub(super) mod internals { / (self.bounds.width - scroller.bounds.width); match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + Anchor::Start => percentage, + Anchor::End => 1.0 - percentage, } } else { 0.0 From 76737351ea9e116291112b7d576d9ed4f6bb5c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 18:12:34 +0200 Subject: [PATCH 098/657] Re-export variants of `Length` and `alignment` types --- core/src/alignment.rs | 25 ------------- core/src/widget/text.rs | 33 +---------------- examples/arc/src/main.rs | 7 +--- examples/bezier_tool/src/main.rs | 11 +++--- examples/clock/src/main.rs | 12 ++---- examples/color_palette/src/main.rs | 11 ++---- examples/combo_box/src/main.rs | 6 +-- examples/component/src/main.rs | 17 +++------ examples/counter/src/main.rs | 3 +- examples/custom_quad/src/main.rs | 4 +- examples/custom_shader/src/main.rs | 9 ++--- examples/custom_widget/src/main.rs | 4 +- examples/download_progress/src/main.rs | 10 ++--- examples/editor/src/main.rs | 6 +-- examples/events/src/main.rs | 6 +-- examples/exit/src/main.rs | 4 +- examples/ferris/src/main.rs | 18 ++++----- examples/game_of_life/src/main.rs | 20 ++++------ examples/gradient/src/main.rs | 10 ++--- examples/integration/src/controls.rs | 5 +-- examples/layout/src/main.rs | 18 ++++----- examples/lazy/src/main.rs | 4 +- examples/loading_spinners/src/main.rs | 6 +-- examples/loupe/src/main.rs | 4 +- examples/modal/src/main.rs | 10 ++--- examples/multi_window/src/main.rs | 6 +-- examples/multitouch/src/main.rs | 7 +--- examples/pane_grid/src/main.rs | 18 ++++----- examples/pick_list/src/main.rs | 6 +-- examples/pokedex/src/main.rs | 19 +++++----- examples/qr_code/src/main.rs | 6 +-- examples/screenshot/src/main.rs | 35 ++++++++++-------- examples/scrollable/src/main.rs | 22 +++++------ examples/sierpinski_triangle/src/main.rs | 8 ++-- examples/slider/src/main.rs | 6 +-- examples/solar_system/src/main.rs | 7 +--- examples/stopwatch/src/main.rs | 6 +-- examples/styling/src/main.rs | 11 +++--- examples/svg/src/main.rs | 26 +++++++------ examples/the_matrix/src/main.rs | 8 +--- examples/toast/src/main.rs | 20 +++++----- examples/todos/src/main.rs | 37 ++++++++++--------- examples/tour/src/main.rs | 26 ++++++------- examples/vectorial_text/src/main.rs | 6 +-- examples/visible_bounds/src/main.rs | 9 +++-- examples/websocket/src/main.rs | 12 +++--- src/lib.rs | 5 +++ widget/src/column.rs | 15 -------- widget/src/container.rs | 47 ++++-------------------- widget/src/helpers.rs | 4 +- widget/src/row.rs | 15 -------- 51 files changed, 255 insertions(+), 395 deletions(-) diff --git a/core/src/alignment.rs b/core/src/alignment.rs index cacf7ce3..8f01ef71 100644 --- a/core/src/alignment.rs +++ b/core/src/alignment.rs @@ -1,30 +1,5 @@ //! Align and position widgets. -/// Returns a value representing center alignment. -pub const fn center() -> Alignment { - Alignment::Center -} - -/// Returns a value representing left alignment. -pub const fn left() -> Horizontal { - Horizontal::Left -} - -/// Returns a value representing right alignment. -pub const fn right() -> Horizontal { - Horizontal::Right -} - -/// Returns a value representing top alignment. -pub const fn top() -> Vertical { - Vertical::Top -} - -/// Returns a value representing bottom alignment. -pub const fn bottom() -> Vertical { - Vertical::Bottom -} - /// Alignment on the axis of a container. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Alignment { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 6ae95c8b..990c5567 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -88,37 +88,8 @@ where /// Centers the [`Text`], both horizontally and vertically. pub fn center(self) -> Self { - self.center_x().center_y() - } - - /// Centers the [`Text`] horizontally. - pub fn center_x(self) -> Self { - self.align_x(alignment::center()) - } - - /// Aligns the [`Text`] to the left, the default. - pub fn align_left(self) -> Self { - self.align_x(alignment::left()) - } - - /// Aligns the [`Text`] to the right. - pub fn align_right(self) -> Self { - self.align_x(alignment::right()) - } - - /// Centers the [`Text`] vertically. - pub fn center_y(self) -> Self { - self.align_y(alignment::center()) - } - - /// Aligns the [`Text`] to the top, the default. - pub fn align_top(self) -> Self { - self.align_y(alignment::top()) - } - - /// Aligns the [`Text`] to the bottom. - pub fn align_bottom(self) -> Self { - self.align_y(alignment::bottom()) + self.align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) } /// Sets the [`alignment::Horizontal`] of the [`Text`]. diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index b1e8402a..18873259 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -4,7 +4,7 @@ use iced::mouse; use iced::widget::canvas::{ self, stroke, Cache, Canvas, Geometry, Path, Stroke, }; -use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme}; +use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme}; pub fn main() -> iced::Result { iced::application("Arc - Iced", Arc::update, Arc::view) @@ -30,10 +30,7 @@ impl Arc { } fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 2e5490e2..949bfad7 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,6 +1,6 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::widget::{button, container, horizontal_space, hover}; -use iced::{Element, Length, Theme}; +use iced::{Element, Fill, Theme}; pub fn main() -> iced::Result { iced::application("Bezier Tool - Iced", Example::update, Example::view) @@ -47,8 +47,7 @@ impl Example { .on_press(Message::Clear), ) .padding(10) - .width(Length::Fill) - .align_right() + .align_right(Fill) }, )) .padding(20) @@ -60,7 +59,7 @@ mod bezier { use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; - use iced::{Element, Length, Point, Rectangle, Renderer, Theme}; + use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; #[derive(Default)] pub struct State { @@ -73,8 +72,8 @@ mod bezier { state: self, curves, }) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .into() } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 4584a0c7..ef3064c7 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -4,7 +4,7 @@ use iced::time; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ - Degrees, Element, Font, Length, Point, Rectangle, Renderer, Subscription, + Degrees, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme, Vector, }; @@ -43,15 +43,9 @@ impl Clock { } fn view(&self) -> Element<Message> { - let canvas = canvas(self as &Self) - .width(Length::Fill) - .height(Length::Fill); + let canvas = canvas(self as &Self).width(Fill).height(Fill); - container(canvas) - .width(Length::Fill) - .height(Length::Fill) - .padding(20) - .into() + container(canvas).padding(20).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 870e4ca7..7f21003b 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -3,8 +3,8 @@ use iced::mouse; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size, - Vector, + Center, Color, Element, Fill, Font, Pixels, Point, Rectangle, Renderer, + Size, Vector, }; use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue}; use std::marker::PhantomData; @@ -150,10 +150,7 @@ impl Theme { } pub fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } fn draw(&self, frame: &mut Frame, text_color: Color) { @@ -320,7 +317,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> { text(color.to_string()).width(185).size(12), ] .spacing(10) - .center_y() + .align_y(Center) .into() } } diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index 0b321472..af53b17a 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,7 +1,7 @@ use iced::widget::{ center, column, combo_box, scrollable, text, vertical_space, }; -use iced::{Element, Length}; +use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { iced::run("Combo Box - Iced", Example::update, Example::view) @@ -64,8 +64,8 @@ impl Example { combo_box, vertical_space().height(150), ] - .width(Length::Fill) - .center_x() + .width(Fill) + .align_x(Center) .spacing(10); center(scrollable(content)).into() diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 14d3d9ba..a5d2e508 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -35,7 +35,7 @@ impl Component { mod numeric_input { use iced::widget::{button, component, row, text, text_input, Component}; - use iced::{Element, Length, Size}; + use iced::{Center, Element, Fill, Length, Size}; pub struct NumericInput<Message> { value: Option<u32>, @@ -103,15 +103,10 @@ mod numeric_input { fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> { let button = |label, on_press| { - button( - text(label) - .width(Length::Fill) - .height(Length::Fill) - .center(), - ) - .width(40) - .height(40) - .on_press(on_press) + button(text(label).width(Fill).height(Fill).center()) + .width(40) + .height(40) + .on_press(on_press) }; row![ @@ -128,7 +123,7 @@ mod numeric_input { .padding(10), button("+", Event::IncrementPressed), ] - .center_y() + .align_y(Center) .spacing(10) .into() } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 848c4c6c..81684c1c 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,4 +1,5 @@ use iced::widget::{button, column, text, Column}; +use iced::Center; pub fn main() -> iced::Result { iced::run("A cool counter", Counter::update, Counter::view) @@ -34,6 +35,6 @@ impl Counter { button("Decrement").on_press(Message::Decrement) ] .padding(20) - .center_x() + .align_x(Center) } } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index ce7eb4ac..8f0c20aa 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -82,7 +82,7 @@ mod quad { } use iced::widget::{center, column, slider, text}; -use iced::{Color, Element, Shadow, Vector}; +use iced::{Center, Color, Element, Shadow, Vector}; pub fn main() -> iced::Result { iced::run("Custom Quad - Iced", Example::update, Example::view) @@ -185,7 +185,7 @@ impl Example { .padding(20) .spacing(20) .max_width(500) - .center_x(); + .align_x(Center); center(content).into() } diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 3e29e7be..5886f6bb 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -6,7 +6,7 @@ use iced::time::Instant; use iced::widget::shader::wgpu; use iced::widget::{center, checkbox, column, row, shader, slider, text}; use iced::window; -use iced::{Color, Element, Length, Subscription}; +use iced::{Center, Color, Element, Fill, Subscription}; fn main() -> iced::Result { iced::application( @@ -122,12 +122,11 @@ impl IcedCubes { let controls = column![top_controls, bottom_controls,] .spacing(10) .padding(20) - .center_x(); + .align_x(Center); - let shader = - shader(&self.scene).width(Length::Fill).height(Length::Fill); + let shader = shader(&self.scene).width(Fill).height(Fill); - center(column![shader, controls].center_x()).into() + center(column![shader, controls].align_x(Center)).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 9f5dcfd0..3b9b9d68 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -83,7 +83,7 @@ mod circle { use circle::circle; use iced::widget::{center, column, slider, text}; -use iced::Element; +use iced::{Center, Element}; pub fn main() -> iced::Result { iced::run("Custom Widget - Iced", Example::update, Example::view) @@ -120,7 +120,7 @@ impl Example { .padding(20) .spacing(20) .max_width(500) - .center_x(); + .align_x(Center); center(content).into() } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 8064b4ae..667fb448 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,7 +1,7 @@ mod download; use iced::widget::{button, center, column, progress_bar, text, Column}; -use iced::{Element, Subscription}; +use iced::{Center, Element, Right, Subscription}; pub fn main() -> iced::Result { iced::application( @@ -69,7 +69,7 @@ impl Example { .padding(10), ) .spacing(20) - .align_right(); + .align_x(Right); center(downloads).padding(20).into() } @@ -160,7 +160,7 @@ impl Download { State::Finished => { column!["Download finished!", button("Start again")] .spacing(10) - .center_x() + .align_x(Center) .into() } State::Downloading { .. } => { @@ -171,14 +171,14 @@ impl Download { button("Try again").on_press(Message::Download(self.id)), ] .spacing(10) - .center_x() + .align_x(Center) .into(), }; Column::new() .spacing(10) .padding(10) - .center_x() + .align_x(Center) .push(progress_bar) .push(control) .into() diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index e24c4ab6..71b1a719 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Element, Font, Length, Subscription, Task, Theme}; +use iced::{Center, Element, Fill, Font, Subscription, Task, Theme}; use std::ffi; use std::io; @@ -158,7 +158,7 @@ impl Editor { .padding([5, 10]) ] .spacing(10) - .center_y(); + .align_y(Center); let status = row![ text(if let Some(path) = &self.file { @@ -184,7 +184,7 @@ impl Editor { column![ controls, text_editor(&self.content) - .height(Length::Fill) + .height(Fill) .on_action(Message::ActionPerformed) .highlight::<Highlighter>( highlighter::Settings { diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 7c9e78a6..5bada9b5 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,7 +1,7 @@ use iced::event::{self, Event}; use iced::widget::{button, center, checkbox, text, Column}; use iced::window; -use iced::{Element, Length, Subscription, Task}; +use iced::{Center, Element, Fill, Subscription, Task}; pub fn main() -> iced::Result { iced::application("Events - Iced", Events::update, Events::view) @@ -66,13 +66,13 @@ impl Events { let toggle = checkbox("Listen to runtime events", self.enabled) .on_toggle(Message::Toggled); - let exit = button(text("Exit").width(Length::Fill).center_x()) + let exit = button(text("Exit").width(Fill).align_x(Center)) .width(100) .padding(10) .on_press(Message::Exit); let content = Column::new() - .center_x() + .align_x(Center) .spacing(20) .push(events) .push(toggle) diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 03ddfb2c..48b0864c 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{button, center, column}; use iced::window; -use iced::{Element, Task}; +use iced::{Center, Element, Task}; pub fn main() -> iced::Result { iced::application("Exit - Iced", Exit::update, Exit::view).run() @@ -44,7 +44,7 @@ impl Exit { ] } .spacing(10) - .center_x(); + .align_x(Center); center(content).padding(20).into() } diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 5d468de1..eaf51354 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -4,8 +4,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Color, ContentFit, Degrees, Element, Length, Radians, Rotation, - Subscription, Theme, + Bottom, Center, Color, ContentFit, Degrees, Element, Fill, Radians, + Rotation, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -108,7 +108,7 @@ impl Image { "I am Ferris!" ] .spacing(20) - .center_x(); + .align_x(Center); let fit = row![ pick_list( @@ -122,7 +122,7 @@ impl Image { Some(self.content_fit), Message::ContentFitChanged ) - .width(Length::Fill), + .width(Fill), pick_list( [RotationStrategy::Floating, RotationStrategy::Solid], Some(match self.rotation { @@ -131,10 +131,10 @@ impl Image { }), Message::RotationStrategyChanged, ) - .width(Length::Fill), + .width(Fill), ] .spacing(10) - .align_bottom(); + .align_y(Bottom); let properties = row![ with_value( @@ -159,12 +159,12 @@ impl Image { .size(12) ] .spacing(10) - .center_y(), + .align_y(Center), format!("Rotation: {:.0}°", f32::from(self.rotation.degrees())) ) ] .spacing(10) - .align_bottom(); + .align_y(Bottom); container(column![fit, center(i_am_ferris), properties].spacing(10)) .padding(10) @@ -206,6 +206,6 @@ fn with_value<'a>( ) -> Element<'a, Message> { column![control.into(), text(value).size(12).line_height(1.0)] .spacing(2) - .center_x() + .align_x(Center) .into() } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 38eb692b..9dcebecc 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -9,7 +9,7 @@ use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, }; -use iced::{Element, Length, Subscription, Task, Theme}; +use iced::{Center, Element, Fill, Subscription, Task, Theme}; use std::time::Duration; pub fn main() -> iced::Result { @@ -135,12 +135,9 @@ impl GameOfLife { .map(move |message| Message::Grid(message, version)), controls, ] - .height(Length::Fill); + .height(Fill); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .into() + container(content).width(Fill).height(Fill).into() } } @@ -169,7 +166,7 @@ fn view_controls<'a>( slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), text!("x{speed}").size(16), ] - .center_y() + .align_y(Center) .spacing(10); row![ @@ -186,7 +183,7 @@ fn view_controls<'a>( ] .padding(10) .spacing(20) - .center_y() + .align_y(Center) .into() } @@ -199,7 +196,7 @@ mod grid { use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; use iced::{ - Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, + Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -333,10 +330,7 @@ mod grid { } pub fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } pub fn clear(&mut self) { diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 018bab82..b2de069f 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -3,7 +3,7 @@ use iced::gradient; use iced::widget::{ checkbox, column, container, horizontal_space, row, slider, text, }; -use iced::{Color, Element, Length, Radians, Theme}; +use iced::{Center, Color, Element, Fill, Radians, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); @@ -67,8 +67,8 @@ impl Gradient { gradient.into() }) - .width(Length::Fill) - .height(Length::Fill); + .width(Fill) + .height(Fill); let angle_picker = row![ text("Angle").width(64), @@ -77,7 +77,7 @@ impl Gradient { ] .spacing(8) .padding(8) - .center_y(); + .align_y(Center); let transparency_toggle = iced::widget::Container::new( checkbox("Transparent window", transparent) @@ -129,6 +129,6 @@ fn color_picker(label: &str, color: Color) -> Element<'_, Color> { ] .spacing(8) .padding(8) - .center_y() + .align_y(Center) .into() } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index b03aa6d4..0b11a323 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,6 +1,6 @@ use iced_wgpu::Renderer; use iced_widget::{column, container, row, slider, text, text_input}; -use iced_winit::core::{Color, Element, Length, Theme}; +use iced_winit::core::{Color, Element, Length::*, Theme}; use iced_winit::runtime::{Program, Task}; pub struct Controls { @@ -85,8 +85,7 @@ impl Program for Controls { .spacing(10), ) .padding(10) - .height(Length::Fill) - .align_bottom() + .align_bottom(Fill) .into() } } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 2bc7fb30..d0827fad 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ pick_list, row, scrollable, text, }; use iced::{ - color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, - Theme, + color, Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, + Subscription, Theme, }; pub fn main() -> iced::Result { @@ -74,7 +74,7 @@ impl Layout { pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected), ] .spacing(20) - .center_y(); + .align_y(Center); let example = center(if self.explain { self.example.view().explain(color!(0x0000ff)) @@ -234,7 +234,7 @@ fn application<'a>() -> Element<'a, Message> { square(40), ] .padding(10) - .center_y(), + .align_y(Center), ) .style(|theme| { let palette = theme.extended_palette(); @@ -248,10 +248,10 @@ fn application<'a>() -> Element<'a, Message> { .spacing(40) .padding(10) .width(200) - .center_x(), + .align_x(Center), ) .style(container::rounded_box) - .center_y(Length::Fill); + .center_y(Fill); let content = container( scrollable( @@ -263,10 +263,10 @@ fn application<'a>() -> Element<'a, Message> { "The end" ] .spacing(40) - .center_x() - .width(Length::Fill), + .align_x(Center) + .width(Fill), ) - .height(Length::Fill), + .height(Fill), ) .padding(10); diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index f24c0d62..8f756210 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{ button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, }; -use iced::{Element, Length}; +use iced::{Element, Fill}; use std::collections::HashSet; use std::hash::Hash; @@ -187,7 +187,7 @@ impl App { }); column![ - scrollable(options).height(Length::Fill), + scrollable(options).height(Fill), row![ text_input("Add a new option", &self.input) .on_input(Message::InputChanged) diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 7fa7ac97..3b178148 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, column, row, slider, text}; -use iced::Element; +use iced::{Center, Element}; use std::time::Duration; @@ -67,7 +67,7 @@ impl LoadingSpinners { Duration::from_secs_f32(self.cycle_duration) ) ] - .center_y() + .align_y(Center) .spacing(20.0), ) }) @@ -83,7 +83,7 @@ impl LoadingSpinners { .width(200.0), text!("{:.2}s", self.cycle_duration), ] - .center_y() + .align_y(Center) .spacing(20.0), ), ) diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 8c9b4def..1c748d42 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{button, center, column, text}; -use iced::Element; +use iced::{Center, Element}; use loupe::loupe; @@ -39,7 +39,7 @@ impl Loupe { button("Decrement").on_press(Message::Decrement) ] .padding(20) - .center_x(), + .align_x(Center), )) .into() } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 6f0f7182..f1f0e8ad 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ self, button, center, column, container, horizontal_space, mouse_area, opaque, pick_list, row, stack, text, text_input, }; -use iced::{Color, Element, Length, Subscription, Task}; +use iced::{Bottom, Color, Element, Fill, Subscription, Task}; use std::fmt; @@ -96,17 +96,17 @@ impl App { let content = container( column![ row![text("Top Left"), horizontal_space(), text("Top Right")] - .height(Length::Fill), + .height(Fill), center(button(text("Show Modal")).on_press(Message::ShowModal)), row![ text("Bottom Left"), horizontal_space(), text("Bottom Right") ] - .align_bottom() - .height(Length::Fill), + .align_y(Bottom) + .height(Fill), ] - .height(Length::Fill), + .height(Fill), ) .padding(10); diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index b1276320..3dcb58f5 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ text_input, }; use iced::window; -use iced::{Element, Length, Subscription, Task, Theme, Vector}; +use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector}; use std::collections::BTreeMap; @@ -188,8 +188,8 @@ impl Window { let content = scrollable( column![scale_input, title_input, new_window_button] .spacing(50) - .width(Length::Fill) - .center_x(), + .width(Fill) + .align_x(Center), ); container(content).center_x(200).into() diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 69717310..a0105a8a 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -6,7 +6,7 @@ use iced::touch; use iced::widget::canvas::event; use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{self, Canvas, Geometry}; -use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme}; +use iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme}; use std::collections::HashMap; @@ -46,10 +46,7 @@ impl Multitouch { } fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + 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 1d1efeeb..f18fc5f3 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{ button, column, container, responsive, row, scrollable, text, }; -use iced::{Color, Element, Length, Size, Subscription}; +use iced::{Center, Color, Element, Fill, Size, Subscription}; pub fn main() -> iced::Result { iced::application("Pane Grid - Iced", Example::update, Example::view) @@ -177,16 +177,16 @@ impl Example { style::pane_active }) }) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .spacing(10) .on_click(Message::Clicked) .on_drag(Message::Dragged) .on_resize(10, Message::Resized); container(pane_grid) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .padding(10) .into() } @@ -254,8 +254,8 @@ fn view_content<'a>( size: Size, ) -> Element<'a, Message> { let button = |label, message| { - button(text(label).width(Length::Fill).center_x().size(16)) - .width(Length::Fill) + button(text(label).width(Fill).align_x(Center).size(16)) + .width(Fill) .padding(8) .on_press(message) }; @@ -281,10 +281,10 @@ fn view_content<'a>( let content = column![text!("{}x{}", size.width, size.height).size(24), controls,] .spacing(10) - .center_x(); + .align_x(Center); container(scrollable(content)) - .center_y(Length::Fill) + .center_y(Fill) .padding(5) .into() } diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 038204f0..d8b2b389 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{column, pick_list, scrollable, vertical_space}; -use iced::{Element, Length}; +use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { iced::run("Pick List - Iced", Example::update, Example::view) @@ -38,8 +38,8 @@ impl Example { pick_list, vertical_space().height(600), ] - .width(Length::Fill) - .center_x() + .width(Fill) + .align_x(Center) .spacing(10); scrollable(content).into() diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 8131bb7e..2e972f6b 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,6 @@ use iced::futures; use iced::widget::{self, center, column, image, row, text}; -use iced::{Element, Length, Task}; +use iced::{Center, Element, Fill, Right, Task}; pub fn main() -> iced::Result { iced::application(Pokedex::title, Pokedex::update, Pokedex::view) @@ -63,10 +63,9 @@ impl Pokedex { } fn view(&self) -> Element<Message> { - let content = match self { + let content: Element<_> = match self { Pokedex::Loading => { - column![text("Searching for Pokémon...").size(40),] - .width(Length::Shrink) + text("Searching for Pokémon...").size(40).into() } Pokedex::Loaded { pokemon } => column![ pokemon.view(), @@ -74,13 +73,15 @@ impl Pokedex { ] .max_width(500) .spacing(20) - .align_right(), + .align_x(Right) + .into(), Pokedex::Errored => column![ text("Whoops! Something went wrong...").size(40), button("Try again").on_press(Message::Search) ] .spacing(20) - .align_right(), + .align_x(Right) + .into(), }; center(content).into() @@ -103,17 +104,17 @@ impl Pokemon { image::viewer(self.image.clone()), column![ row![ - text(&self.name).size(30).width(Length::Fill), + text(&self.name).size(30).width(Fill), text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]), ] - .center_y() + .align_y(Center) .spacing(20), self.description.as_ref(), ] .spacing(20), ] .spacing(20) - .center_y() + .align_y(Center) .into() } diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 14db776e..f1b654e0 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; -use iced::{Element, Theme}; +use iced::{Center, Element, Theme}; pub fn main() -> iced::Result { iced::application( @@ -58,7 +58,7 @@ impl QRGenerator { pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,) ] .spacing(10) - .center_y(); + .align_y(Center); let content = column![title, input, choose_theme] .push_maybe( @@ -68,7 +68,7 @@ impl QRGenerator { ) .width(700) .spacing(20) - .center_x(); + .align_x(Center); center(content).padding(20).into() } diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 3b583d13..7fc87446 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -2,7 +2,10 @@ use iced::keyboard; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; -use iced::{ContentFit, Element, Length, Rectangle, Subscription, Task}; +use iced::{ + Center, ContentFit, Element, Fill, FillPortion, Rectangle, Subscription, + Task, +}; use ::image as img; use ::image::ColorType; @@ -111,15 +114,15 @@ impl Example { screenshot.clone(), )) .content_fit(ContentFit::Contain) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .into() } else { text("Press the button to take a screenshot!").into() }; let image = container(image) - .center_y(Length::FillPortion(2)) + .center_y(FillPortion(2)) .padding(10) .style(container::rounded_box); @@ -130,7 +133,7 @@ impl Example { numeric_input("0", self.y_input_value).map(Message::YInputChanged) ] .spacing(10) - .center_y(); + .align_y(Center); let crop_dimension_controls = row![ text("W:").width(30), @@ -141,7 +144,7 @@ impl Example { .map(Message::HeightInputChanged) ] .spacing(10) - .center_y(); + .align_y(Center); let crop_controls = column![crop_origin_controls, crop_dimension_controls] @@ -151,7 +154,7 @@ impl Example { .map(|error| text!("Crop error! \n{error}")), ) .spacing(10) - .center_x(); + .align_x(Center); let controls = { let save_result = @@ -168,7 +171,7 @@ impl Example { column![ button(centered_text("Screenshot!")) .padding([10, 20, 10, 20]) - .width(Length::Fill) + .width(Fill) .on_press(Message::Screenshot), if !self.png_saving { button(centered_text("Save as png")).on_press_maybe( @@ -180,7 +183,7 @@ impl Example { } .style(button::secondary) .padding([10, 20, 10, 20]) - .width(Length::Fill) + .width(Fill) ] .spacing(10), column![ @@ -189,22 +192,22 @@ impl Example { .on_press(Message::Crop) .style(button::danger) .padding([10, 20, 10, 20]) - .width(Length::Fill), + .width(Fill), ] .spacing(10) - .center_x(), + .align_x(Center), ] .push_maybe(save_result.map(text)) .spacing(40) }; - let side_content = container(controls).center_y(Length::Fill); + let side_content = container(controls).center_y(Fill); let content = row![side_content, image] .spacing(10) - .width(Length::Fill) - .height(Length::Fill) - .center_y(); + .width(Fill) + .height(Fill) + .align_y(Center); container(content).padding(10).into() } @@ -265,5 +268,5 @@ fn numeric_input( } fn centered_text(content: &str) -> Element<'_, Message> { - text(content).width(Length::Fill).center_x().into() + text(content).width(Fill).align_x(Center).into() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index bf8fdedf..a1c23976 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; -use iced::{Border, Color, Element, Length, Task, Theme}; +use iced::{Border, Center, Color, Element, Fill, Task, Theme}; use once_cell::sync::Lazy; @@ -212,7 +212,7 @@ impl ScrollableDemo { text("End!"), scroll_to_beginning_button(), ] - .center_x() + .align_x(Center) .padding([40, 0, 40, 0]) .spacing(40), ) @@ -223,8 +223,8 @@ impl ScrollableDemo { .scroller_width(self.scroller_width) .anchor(self.anchor), )) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), Direction::Horizontal => scrollable( @@ -238,7 +238,7 @@ impl ScrollableDemo { scroll_to_beginning_button(), ] .height(450) - .center_y() + .align_y(Center) .padding([0, 40, 0, 40]) .spacing(40), ) @@ -249,8 +249,8 @@ impl ScrollableDemo { .scroller_width(self.scroller_width) .anchor(self.anchor), )) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), Direction::Multi => scrollable( @@ -280,7 +280,7 @@ impl ScrollableDemo { text("Horizontal - End!"), scroll_to_beginning_button(), ] - .center_y() + .align_y(Center) .padding([0, 40, 0, 40]) .spacing(40), ) @@ -296,8 +296,8 @@ impl ScrollableDemo { vertical: scrollbar, } }) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), }); @@ -322,7 +322,7 @@ impl ScrollableDemo { let content: Element<Message> = column![scroll_controls, scrollable_content, progress_bars] - .center_x() + .align_x(Center) .spacing(10) .into(); diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 159b5597..99e7900a 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -2,7 +2,7 @@ use iced::mouse; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas, Geometry}; use iced::widget::{column, row, slider, text}; -use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme}; +use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme}; use rand::Rng; use std::fmt::Debug; @@ -50,9 +50,7 @@ impl SierpinskiEmulator { fn view(&self) -> iced::Element<'_, Message> { column![ - Canvas::new(&self.graph) - .width(Length::Fill) - .height(Length::Fill), + Canvas::new(&self.graph).width(Fill).height(Fill), row![ text!("Iteration: {:?}", self.graph.iteration), slider(0..=10000, self.graph.iteration, Message::IterationSet) @@ -60,7 +58,7 @@ impl SierpinskiEmulator { .padding(10) .spacing(20), ] - .center_x() + .align_x(Center) .into() } } diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index 5443c645..ffb5475f 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{column, container, iced, slider, text, vertical_slider}; -use iced::{Element, Length}; +use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { iced::run("Slider - Iced", Slider::update, Slider::view) @@ -45,8 +45,8 @@ impl Slider { let text = text(self.value); column![v_slider, h_slider, text, iced(self.value as f32),] - .width(Length::Fill) - .center_x() + .width(Fill) + .align_x(Center) .spacing(20) .padding(20) .into() diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 2a67e23e..a6f1ba6f 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -13,7 +13,7 @@ use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{Geometry, Path}; use iced::window; use iced::{ - Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription, + Color, Element, Fill, Point, Rectangle, Renderer, Size, Subscription, Theme, Vector, }; @@ -52,10 +52,7 @@ impl SolarSystem { } fn view(&self) -> Element<Message> { - canvas(&self.state) - .width(Length::Fill) - .height(Length::Fill) - .into() + canvas(&self.state).width(Fill).height(Fill).into() } fn theme(&self) -> Theme { diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index ca7b8f91..0d824d36 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,7 +1,7 @@ use iced::keyboard; use iced::time; use iced::widget::{button, center, column, row, text}; -use iced::{Element, Subscription, Theme}; +use iced::{Center, Element, Subscription, Theme}; use std::time::{Duration, Instant}; @@ -101,7 +101,7 @@ impl Stopwatch { .size(40); let button = - |label| button(text(label).center_x()).padding(10).width(80); + |label| button(text(label).align_x(Center)).padding(10).width(80); let toggle_button = { let label = match self.state { @@ -118,7 +118,7 @@ impl Stopwatch { let controls = row![toggle_button, reset_button].spacing(20); - let content = column![duration, controls].center_x().spacing(20); + let content = column![duration, controls].align_x(Center).spacing(20); center(content).into() } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 8f979ea8..527aaa29 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ row, scrollable, slider, text, text_input, toggler, vertical_rule, vertical_space, }; -use iced::{Element, Length, Theme}; +use iced::{Center, Element, Fill, Theme}; pub fn main() -> iced::Result { iced::application("Styling - Iced", Styling::update, Styling::view) @@ -48,7 +48,7 @@ impl Styling { let choose_theme = column![ text("Theme:"), pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged) - .width(Length::Fill), + .width(Fill), ] .spacing(10); @@ -71,7 +71,7 @@ impl Styling { vertical_space().height(800), "You did it!" ]) - .width(Length::Fill) + .width(Fill) .height(100); let checkbox = checkbox("Check me!", self.checkbox_value) @@ -82,13 +82,12 @@ impl Styling { self.toggler_value, Message::TogglerToggled, ) - .width(Length::Shrink) .spacing(10); let content = column![ choose_theme, horizontal_rule(38), - row![text_input, button].spacing(10).center_y(), + row![text_input, button].spacing(10).align_y(Center), slider, progress_bar, row![ @@ -98,7 +97,7 @@ impl Styling { ] .spacing(10) .height(100) - .center_y(), + .align_y(Center), ] .spacing(20) .padding(20) diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index e071c3af..02cb85cc 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{center, checkbox, column, container, svg}; -use iced::{color, Element, Length}; +use iced::{color, Element, Fill}; pub fn main() -> iced::Result { iced::run("SVG - Iced", Tiger::update, Tiger::view) @@ -30,24 +30,26 @@ impl Tiger { env!("CARGO_MANIFEST_DIR") )); - let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( - |_theme, _status| svg::Style { - color: if self.apply_color_filter { - Some(color!(0x0000ff)) - } else { - None - }, - }, - ); + let svg = + svg(handle) + .width(Fill) + .height(Fill) + .style(|_theme, _status| svg::Style { + color: if self.apply_color_filter { + Some(color!(0x0000ff)) + } else { + None + }, + }); let apply_color_filter = checkbox("Apply a color filter", self.apply_color_filter) .on_toggle(Message::ToggleColorFilter); center( - column![svg, container(apply_color_filter).center_x(Length::Fill)] + column![svg, container(apply_color_filter).center_x(Fill)] .spacing(20) - .height(Length::Fill), + .height(Fill), ) .padding(20) .into() diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 2ae1cc3a..0ed52dda 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -2,8 +2,7 @@ use iced::mouse; use iced::time::{self, Instant}; use iced::widget::canvas; use iced::{ - Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, - Theme, + Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme, }; use std::cell::RefCell; @@ -37,10 +36,7 @@ impl TheMatrix { } fn view(&self) -> Element<Message> { - canvas(self as &Self) - .width(Length::Fill) - .height(Length::Fill) - .into() + canvas(self as &Self).width(Fill).height(Fill).into() } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index fd7f07c2..040c19bd 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -4,7 +4,7 @@ use iced::keyboard::key; use iced::widget::{ self, button, center, column, pick_list, row, slider, text, text_input, }; -use iced::{Element, Length, Subscription, Task}; +use iced::{Center, Element, Fill, Subscription, Task}; use toast::{Status, Toast}; @@ -125,7 +125,7 @@ impl App { Some(self.editing.status), Message::Status ) - .width(Length::Fill) + .width(Fill) .into() ), subtitle( @@ -142,7 +142,7 @@ impl App { .spacing(5) .into() ), - column![add_toast].center_x() + column![add_toast].align_x(Center) ] .spacing(10) .max_width(200), @@ -177,8 +177,8 @@ mod toast { }; use iced::window; use iced::{ - Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme, - Vector, + Alignment, Center, Element, Fill, Length, Point, Rectangle, Renderer, + Size, Theme, Vector, }; pub const DEFAULT_TIMEOUT: u64 = 5; @@ -245,9 +245,9 @@ mod toast { .on_press((on_close)(index)) .padding(3), ] - .center_y() + .align_y(Center) ) - .width(Length::Fill) + .width(Fill) .padding(5) .style(match toast.status { Status::Primary => primary, @@ -257,7 +257,7 @@ mod toast { }), horizontal_rule(1), container(text(toast.body.as_str())) - .width(Length::Fill) + .width(Fill) .padding(5) .style(container::rounded_box), ]) @@ -479,8 +479,8 @@ mod toast { layout::flex::Axis::Vertical, renderer, &limits, - Length::Fill, - Length::Fill, + Fill, + Fill, 10.into(), 10.0, Alignment::End, diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index af651ee2..86845f87 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ scrollable, text, text_input, Text, }; use iced::window; -use iced::{Element, Font, Length, Subscription, Task as Command}; +use iced::{Center, Element, Fill, Font, Subscription, Task as Command}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -192,10 +192,10 @@ impl Todos { .. }) => { let title = text("todos") - .width(Length::Fill) + .width(Fill) .size(100) .color([0.5, 0.5, 0.5]) - .center_x(); + .align_x(Center); let input = text_input("What needs to be done?", input_value) .id(INPUT_ID.clone()) @@ -239,10 +239,7 @@ impl Todos { .spacing(20) .max_width(800); - scrollable( - container(content).center_x(Length::Fill).padding(40), - ) - .into() + scrollable(container(content).center_x(Fill).padding(40)).into() } } } @@ -342,7 +339,7 @@ impl Task { TaskState::Idle => { let checkbox = checkbox(&self.description, self.completed) .on_toggle(TaskMessage::Completed) - .width(Length::Fill) + .width(Fill) .size(17) .text_shaping(text::Shaping::Advanced); @@ -354,7 +351,7 @@ impl Task { .style(button::text), ] .spacing(20) - .center_y() + .align_y(Center) .into() } TaskState::Editing => { @@ -368,14 +365,16 @@ impl Task { row![ text_input, button( - row![delete_icon(), "Delete"].spacing(10).center_y() + row![delete_icon(), "Delete"] + .spacing(10) + .align_y(Center) ) .on_press(TaskMessage::Delete) .padding(10) .style(button::danger) ] .spacing(20) - .center_y() + .align_y(Center) .into() } } @@ -402,17 +401,16 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { "{tasks_left} {} left", if tasks_left == 1 { "task" } else { "tasks" } ) - .width(Length::Fill), + .width(Fill), row![ filter_button("All", Filter::All, current_filter), filter_button("Active", Filter::Active, current_filter), filter_button("Completed", Filter::Completed, current_filter,), ] - .width(Length::Shrink) .spacing(10) ] .spacing(20) - .center_y() + .align_y(Center) .into() } @@ -437,15 +435,15 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - center(text("Loading...").center_x().size(50)).into() + center(text("Loading...").width(Fill).align_x(Center).size(50)).into() } fn empty_message(message: &str) -> Element<'_, Message> { center( text(message) - .width(Length::Fill) + .width(Fill) .size(25) - .center_x() + .align_x(Center) .color([0.7, 0.7, 0.7]), ) .height(200) @@ -456,7 +454,10 @@ fn empty_message(message: &str) -> Element<'_, Message> { const ICONS: Font = Font::with_name("Iced-Todos-Icons"); fn icon(unicode: char) -> Text<'static> { - text(unicode.to_string()).font(ICONS).width(20).center_x() + text(unicode.to_string()) + .font(ICONS) + .width(20) + .align_x(Center) } fn edit_icon() -> Text<'static> { diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 941c5b33..ee4754e6 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{ scrollable, slider, text, text_input, toggler, vertical_space, }; use iced::widget::{Button, Column, Container, Slider}; -use iced::{Color, Element, Font, Length, Pixels}; +use iced::{Center, Color, Element, Fill, Font, Pixels}; pub fn main() -> iced::Result { #[cfg(target_arch = "wasm32")] @@ -172,10 +172,10 @@ impl Tour { } else { content }) - .center_x(Length::Fill), + .center_x(Fill), ); - container(scrollable).center_y(Length::Fill).into() + container(scrollable).center_y(Fill).into() } fn can_continue(&self) -> bool { @@ -234,7 +234,7 @@ impl Tour { 0 to 100:", ) .push(slider(0..=100, self.slider, Message::SliderChanged)) - .push(text(self.slider.to_string()).width(Length::Fill).center_x()) + .push(text(self.slider.to_string()).width(Fill).align_x(Center)) } fn rows_and_columns(&self) -> Column<Message> { @@ -263,7 +263,7 @@ impl Tour { let spacing_section = column![ slider(0..=80, self.spacing, Message::SpacingChanged), - text!("{} px", self.spacing).width(Length::Fill).center_x(), + text!("{} px", self.spacing).width(Fill).align_x(Center), ] .spacing(10); @@ -374,7 +374,7 @@ impl Tour { .push("An image that tries to keep its aspect ratio.") .push(ferris(width, filter_method)) .push(slider(100..=500, width, Message::ImageWidthChanged)) - .push(text!("Width: {width} px").width(Length::Fill).center_x()) + .push(text!("Width: {width} px").width(Fill).align_x(Center)) .push( checkbox( "Use nearest interpolation", @@ -382,7 +382,7 @@ impl Tour { ) .on_toggle(Message::ImageUseNearestToggled), ) - .center_x() + .align_x(Center) } fn scrollable(&self) -> Column<Message> { @@ -398,13 +398,13 @@ impl Tour { .push(vertical_space().height(4096)) .push( text("You are halfway there!") - .width(Length::Fill) + .width(Fill) .size(30) - .center_x(), + .align_x(Center), ) .push(vertical_space().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) - .push(text("You made it!").width(Length::Fill).size(50).center_x()) + .push(text("You made it!").width(Fill).size(50).align_x(Center)) } fn text_input(&self) -> Column<Message> { @@ -448,8 +448,8 @@ impl Tour { } else { value }) - .width(Length::Fill) - .center_x(), + .width(Fill) + .align_x(Center), ) } @@ -554,7 +554,7 @@ fn ferris<'a>( .filter_method(filter_method) .width(width), ) - .center_x(Length::Fill) + .center_x(Fill) } fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> { diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index f7af486a..ce34d826 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -3,7 +3,7 @@ use iced::mouse; use iced::widget::{ canvas, checkbox, column, horizontal_space, row, slider, text, }; -use iced::{Element, Length, Point, Rectangle, Renderer, Theme, Vector}; +use iced::{Center, Element, Fill, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { iced::application( @@ -59,7 +59,7 @@ impl VectorialText { }; column![ - canvas(&self.state).width(Length::Fill).height(Length::Fill), + canvas(&self.state).width(Fill).height(Fill), column![ checkbox("Use Japanese", self.state.use_japanese,) .on_toggle(Message::ToggleJapanese), @@ -85,7 +85,7 @@ impl VectorialText { ] .spacing(20), ] - .center_x() + .align_x(Center) .spacing(10) ] .spacing(10) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 245600c5..77fec65e 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -5,7 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Color, Element, Font, Length, Point, Rectangle, Subscription, Task, Theme, + Center, Color, Element, Fill, Font, Point, Rectangle, Subscription, Task, + Theme, }; pub fn main() -> iced::Result { @@ -69,7 +70,7 @@ impl Example { .color_maybe(color), ] .height(40) - .center_y() + .align_y(Center) }; let view_bounds = |label, bounds: Option<Rectangle>| { @@ -129,13 +130,13 @@ impl Example { .padding(20) ) .on_scroll(|_| Message::Scrolled) - .width(Length::Fill) + .width(Fill) .height(300), ] .padding(20) ) .on_scroll(|_| Message::Scrolled) - .width(Length::Fill) + .width(Fill) .height(300), ] .spacing(10) diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 1bac61aa..8b1efb41 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -3,7 +3,7 @@ mod echo; use iced::widget::{ self, button, center, column, row, scrollable, text, text_input, }; -use iced::{color, Element, Length, Subscription, Task}; +use iced::{color, Center, Element, Fill, Subscription, Task}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { @@ -103,7 +103,7 @@ impl WebSocket { .spacing(10), ) .id(MESSAGE_LOG.clone()) - .height(Length::Fill) + .height(Fill) .into() }; @@ -112,8 +112,8 @@ impl WebSocket { .on_input(Message::NewMessageChanged) .padding(10); - let mut button = - button(text("Send").height(40).center_y()).padding([0, 20]); + let mut button = button(text("Send").height(40).align_y(Center)) + .padding([0, 20]); if matches!(self.state, State::Connected(_)) { if let Some(message) = echo::Message::new(&self.new_message) { @@ -122,11 +122,11 @@ impl WebSocket { } } - row![input, button].spacing(10).center_y() + row![input, button].spacing(10).align_y(Center) }; column![message_log, new_message_input] - .height(Length::Fill) + .height(Fill) .padding(20) .spacing(10) .into() diff --git a/src/lib.rs b/src/lib.rs index 79e2f276..8c6aeea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,6 +205,11 @@ pub use crate::core::{ pub use crate::runtime::exit; pub use iced_futures::Subscription; +pub use alignment::Horizontal::{Left, Right}; +pub use alignment::Vertical::{Bottom, Top}; +pub use Alignment::Center; +pub use Length::{Fill, FillPortion, Shrink}; + pub mod task { //! Create runtime tasks. pub use crate::runtime::task::{Handle, Task}; diff --git a/widget/src/column.rs b/widget/src/column.rs index ef4ee99d..ae82ccaa 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -104,21 +104,6 @@ where self } - /// Centers the contents of the [`Column`] horizontally. - pub fn center_x(self) -> Self { - self.align_x(Alignment::Center) - } - - /// Aligns the contents of the [`Column`] to the left. - pub fn align_left(self) -> Self { - self.align_x(alignment::left()) - } - - /// Aligns the contents of the [`Column`] to the right. - pub fn align_right(self) -> Self { - self.align_x(alignment::right()) - } - /// Sets the horizontal alignment of the contents of the [`Column`] . pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self { self.align = Alignment::from(align.into()); diff --git a/widget/src/container.rs b/widget/src/container.rs index adfe347c..cf27bf96 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -92,37 +92,6 @@ where self } - /// Sets the [`Container`] to fill the available space in the horizontal axis. - /// - /// Calling this method is equivalent to calling [`width`] with a - /// [`Length::Fill`]. - /// - /// [`width`]: Self::width - pub fn fill_x(self) -> Self { - self.width(Length::Fill) - } - - /// Sets the [`Container`] to fill the available space in the vertical axis. - /// - /// Calling this method is equivalent to calling [`height`] with a - /// [`Length::Fill`]. - /// - /// [`height`]: Self::height - pub fn fill_y(self) -> Self { - self.height(Length::Fill) - } - - /// Sets the [`Container`] to fill all the available space. - /// - /// Calling this method is equivalent to chaining [`fill_x`] and - /// [`fill_y`]. - /// - /// [`fill_x`]: Self::fill_x - /// [`fill_y`]: Self::fill_y - pub fn fill(self) -> Self { - self.width(Length::Fill).height(Length::Fill) - } - /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { self.max_width = max_width.into().0; @@ -159,23 +128,23 @@ where } /// Aligns the contents of the [`Container`] to the left. - pub fn align_left(self) -> Self { - self.align_x(alignment::left()) + pub fn align_left(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Left) } /// Aligns the contents of the [`Container`] to the right. - pub fn align_right(self) -> Self { - self.align_x(alignment::right()) + pub fn align_right(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Right) } /// Aligns the contents of the [`Container`] to the top. - pub fn align_top(self) -> Self { - self.align_y(alignment::top()) + pub fn align_top(self, height: Length) -> Self { + self.height(height).align_y(alignment::Vertical::Top) } /// Aligns the contents of the [`Container`] to the bottom. - pub fn align_bottom(self) -> Self { - self.align_y(alignment::bottom()) + pub fn align_bottom(self, height: Length) -> Self { + self.height(height).align_y(alignment::Vertical::Bottom) } /// Sets the content alignment for the horizontal axis of the [`Container`]. diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index f27b7807..1f282f54 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -906,7 +906,7 @@ where + 'a, Theme: text::Catalog + crate::svg::Catalog + 'a, { - use crate::core::Font; + use crate::core::{Alignment, Font}; use crate::svg; use once_cell::sync::Lazy; @@ -921,7 +921,7 @@ where text("iced").size(text_size).font(Font::MONOSPACE) ] .spacing(text_size.0 / 3.0) - .center_y() + .align_y(Alignment::Center) .into() } diff --git a/widget/src/row.rs b/widget/src/row.rs index 129feb7e..3feeaa7e 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -95,21 +95,6 @@ where self } - /// Centers the contents of the [`Row`] vertically. - pub fn center_y(self) -> Self { - self.align_y(Alignment::Center) - } - - /// Aligns the contents of the [`Row`] to the top. - pub fn align_top(self) -> Self { - self.align_y(alignment::top()) - } - - /// Aligns the contents of the [`Row`] to the bottom. - pub fn align_bottom(self) -> Self { - self.align_y(alignment::bottom()) - } - /// Sets the vertical alignment of the contents of the [`Row`] . pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self { self.align = Alignment::from(align.into()); From 915c926c28f77ad7b401a17964408d27548543e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 18:18:04 +0200 Subject: [PATCH 099/657] Fix inconsistent `align_*` methods in `Container` --- widget/src/container.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index cf27bf96..92b782e8 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -138,12 +138,12 @@ where } /// Aligns the contents of the [`Container`] to the top. - pub fn align_top(self, height: Length) -> Self { + pub fn align_top(self, height: impl Into<Length>) -> Self { self.height(height).align_y(alignment::Vertical::Top) } /// Aligns the contents of the [`Container`] to the bottom. - pub fn align_bottom(self, height: Length) -> Self { + pub fn align_bottom(self, height: impl Into<Length>) -> Self { self.height(height).align_y(alignment::Vertical::Bottom) } From 7c3341760de74df2153ef367e502960f20f9c681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 18:40:54 +0200 Subject: [PATCH 100/657] Improve `Padding` ergonomics We expose free functions for creating a `Padding` and methods with the same name to modify its fields. --- core/src/lib.rs | 2 +- core/src/padding.rs | 102 +++++++++++++++++++------------- examples/screenshot/src/main.rs | 6 +- examples/scrollable/src/main.rs | 6 +- src/lib.rs | 1 + 5 files changed, 70 insertions(+), 47 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 32156441..40a288e5 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,6 +20,7 @@ pub mod keyboard; pub mod layout; pub mod mouse; pub mod overlay; +pub mod padding; pub mod renderer; pub mod svg; pub mod text; @@ -35,7 +36,6 @@ mod color; mod content_fit; mod element; mod length; -mod padding; mod pixels; mod point; mod rectangle; diff --git a/core/src/padding.rs b/core/src/padding.rs index b8c941d8..a0915fbc 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -1,3 +1,4 @@ +//! Space stuff around the perimeter. use crate::{Pixels, Size}; /// An amount of space to pad for each side of a box @@ -9,7 +10,6 @@ use crate::{Pixels, Size}; /// # /// let padding = Padding::from(20); // 20px on all sides /// let padding = Padding::from([10, 20]); // top/bottom, left/right -/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left /// ``` /// /// Normally, the `padding` method of a widget will ask for an `Into<Padding>`, @@ -31,7 +31,6 @@ use crate::{Pixels, Size}; /// /// let widget = Widget::new().padding(20); // 20px on all sides /// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right -/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left /// ``` #[derive(Debug, Copy, Clone)] pub struct Padding { @@ -45,6 +44,43 @@ pub struct Padding { pub left: f32, } +/// Create a [`Padding`] that is equal on all sides. +pub fn all(padding: impl Into<Pixels>) -> Padding { + Padding::new(padding.into().0) +} + +/// Create some top [`Padding`]. +pub fn top(padding: impl Into<Pixels>) -> Padding { + Padding { + top: padding.into().0, + ..Padding::ZERO + } +} + +/// Create some bottom [`Padding`]. +pub fn bottom(padding: impl Into<Pixels>) -> Padding { + Padding { + bottom: padding.into().0, + ..Padding::ZERO + } +} + +/// Create some left [`Padding`]. +pub fn left(padding: impl Into<Pixels>) -> Padding { + Padding { + left: padding.into().0, + ..Padding::ZERO + } +} + +/// Create some right [`Padding`]. +pub fn right(padding: impl Into<Pixels>) -> Padding { + Padding { + right: padding.into().0, + ..Padding::ZERO + } +} + impl Padding { /// Padding of zero pub const ZERO: Padding = Padding { @@ -64,35 +100,43 @@ impl Padding { } } - /// Create some top [`Padding`]. - pub fn top(padding: impl Into<Pixels>) -> Self { + /// Sets the [`top`] of the [`Padding`]. + /// + /// [`top`]: Self::top + pub fn top(self, top: impl Into<Pixels>) -> Self { Self { - top: padding.into().0, - ..Self::ZERO + top: top.into().0, + ..self } } - /// Create some right [`Padding`]. - pub fn right(padding: impl Into<Pixels>) -> Self { + /// Sets the [`bottom`] of the [`Padding`]. + /// + /// [`bottom`]: Self::bottom + pub fn bottom(self, bottom: impl Into<Pixels>) -> Self { Self { - right: padding.into().0, - ..Self::ZERO + bottom: bottom.into().0, + ..self } } - /// Create some bottom [`Padding`]. - pub fn bottom(padding: impl Into<Pixels>) -> Self { + /// Sets the [`left`] of the [`Padding`]. + /// + /// [`left`]: Self::left + pub fn left(self, left: impl Into<Pixels>) -> Self { Self { - bottom: padding.into().0, - ..Self::ZERO + left: left.into().0, + ..self } } - /// Create some left [`Padding`]. - pub fn left(padding: impl Into<Pixels>) -> Self { + /// Sets the [`right`] of the [`Padding`]. + /// + /// [`right`]: Self::right + pub fn right(self, right: impl Into<Pixels>) -> Self { Self { - left: padding.into().0, - ..Self::ZERO + right: right.into().0, + ..self } } @@ -143,17 +187,6 @@ impl From<[u16; 2]> for Padding { } } -impl From<[u16; 4]> for Padding { - fn from(p: [u16; 4]) -> Self { - Padding { - top: f32::from(p[0]), - right: f32::from(p[1]), - bottom: f32::from(p[2]), - left: f32::from(p[3]), - } - } -} - impl From<f32> for Padding { fn from(p: f32) -> Self { Padding { @@ -176,17 +209,6 @@ impl From<[f32; 2]> for Padding { } } -impl From<[f32; 4]> for Padding { - fn from(p: [f32; 4]) -> Self { - Padding { - top: p[0], - right: p[1], - bottom: p[2], - left: p[3], - } - } -} - impl From<Padding> for Size { fn from(padding: Padding) -> Self { Self::new(padding.horizontal(), padding.vertical()) diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 7fc87446..2d980dd9 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -170,7 +170,7 @@ impl Example { column![ column![ button(centered_text("Screenshot!")) - .padding([10, 20, 10, 20]) + .padding([10, 20]) .width(Fill) .on_press(Message::Screenshot), if !self.png_saving { @@ -182,7 +182,7 @@ impl Example { .style(button::secondary) } .style(button::secondary) - .padding([10, 20, 10, 20]) + .padding([10, 20]) .width(Fill) ] .spacing(10), @@ -191,7 +191,7 @@ impl Example { button(centered_text("Crop")) .on_press(Message::Crop) .style(button::danger) - .padding([10, 20, 10, 20]) + .padding([10, 20]) .width(Fill), ] .spacing(10) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index a1c23976..de4f2f9a 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -213,7 +213,7 @@ impl ScrollableDemo { scroll_to_beginning_button(), ] .align_x(Center) - .padding([40, 0, 40, 0]) + .padding([40, 0]) .spacing(40), ) .direction(scrollable::Direction::Vertical( @@ -239,7 +239,7 @@ impl ScrollableDemo { ] .height(450) .align_y(Center) - .padding([0, 40, 0, 40]) + .padding([0, 40]) .spacing(40), ) .direction(scrollable::Direction::Horizontal( @@ -281,7 +281,7 @@ impl ScrollableDemo { scroll_to_beginning_button(), ] .align_y(Center) - .padding([0, 40, 0, 40]) + .padding([0, 40]) .spacing(40), ) .direction({ diff --git a/src/lib.rs b/src/lib.rs index 8c6aeea3..7ced9a57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,6 +196,7 @@ pub use crate::core::alignment; pub use crate::core::border; pub use crate::core::color; pub use crate::core::gradient; +pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, From ab392cee947a7207bdd021d5f04945b9d5a16b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 19:10:52 +0200 Subject: [PATCH 101/657] Improve `Border` ergonomics --- core/src/border.rs | 164 ++++++++++++++++++++++++----- core/src/padding.rs | 22 +--- examples/custom_quad/src/main.rs | 39 ++++--- examples/custom_widget/src/main.rs | 5 +- widget/src/button.rs | 7 +- widget/src/container.rs | 8 +- widget/src/overlay/menu.rs | 7 +- widget/src/progress_bar.rs | 8 +- widget/src/radio.rs | 7 +- widget/src/rule.rs | 4 +- widget/src/scrollable.rs | 9 +- widget/src/slider.rs | 10 +- widget/src/vertical_slider.rs | 9 +- 13 files changed, 204 insertions(+), 95 deletions(-) diff --git a/core/src/border.rs b/core/src/border.rs index 2df24988..05e74ac6 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -10,40 +10,64 @@ pub struct Border { /// The width of the border. pub width: f32, - /// The radius of the border. + /// The [`Radius`] of the border. pub radius: Radius, } -impl Border { - /// Creates a new default rounded [`Border`] with the given [`Radius`]. - /// - /// ``` - /// # use iced_core::Border; - /// # - /// assert_eq!(Border::rounded(10), Border::default().with_radius(10)); - /// ``` - pub fn rounded(radius: impl Into<Radius>) -> Self { - Self::default().with_radius(radius) - } +/// Creates a new [`Border`] with the given [`Radius`]. +/// +/// ``` +/// # use iced_core::border::{self, Border}; +/// # +/// assert_eq!(border::rounded(10), Border::default().rounded(10)); +/// ``` +pub fn rounded(radius: impl Into<Radius>) -> Border { + Border::default().rounded(radius) +} - /// Updates the [`Color`] of the [`Border`]. - pub fn with_color(self, color: impl Into<Color>) -> Self { +/// Creates a new [`Border`] with the given [`Color`]. +/// +/// ``` +/// # use iced_core::border::{self, Border}; +/// # use iced_core::Color; +/// # +/// assert_eq!(border::color(Color::BLACK), Border::default().color(Color::BLACK)); +/// ``` +pub fn color(color: impl Into<Color>) -> Border { + Border::default().color(color) +} + +/// Creates a new [`Border`] with the given `width`. +/// +/// ``` +/// # use iced_core::border::{self, Border}; +/// # use iced_core::Color; +/// # +/// assert_eq!(border::width(10), Border::default().width(10)); +/// ``` +pub fn width(width: impl Into<Pixels>) -> Border { + Border::default().width(width) +} + +impl Border { + /// Sets the [`Color`] of the [`Border`]. + pub fn color(self, color: impl Into<Color>) -> Self { Self { color: color.into(), ..self } } - /// Updates the [`Radius`] of the [`Border`]. - pub fn with_radius(self, radius: impl Into<Radius>) -> Self { + /// Sets the [`Radius`] of the [`Border`]. + pub fn rounded(self, radius: impl Into<Radius>) -> Self { Self { radius: radius.into(), ..self } } - /// Updates the width of the [`Border`]. - pub fn with_width(self, width: impl Into<Pixels>) -> Self { + /// Sets the width of the [`Border`]. + pub fn width(self, width: impl Into<Pixels>) -> Self { Self { width: width.into().0, ..self @@ -54,11 +78,96 @@ impl Border { /// The border radii for the corners of a graphics primitive in the order: /// top-left, top-right, bottom-right, bottom-left. #[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Radius([f32; 4]); +pub struct Radius { + /// Top left radius + pub top_left: f32, + /// Top right radius + pub top_right: f32, + /// Bottom right radius + pub bottom_right: f32, + /// Bottom left radius + pub bottom_left: f32, +} + +/// Creates a new [`Radius`] with the same value for each corner. +pub fn radius(value: impl Into<Pixels>) -> Radius { + Radius::new(value) +} + +/// Creates a new [`Radius`] with the given top left value. +pub fn top_left(value: impl Into<Pixels>) -> Radius { + Radius::default().top_left(value) +} + +/// Creates a new [`Radius`] with the given top right value. +pub fn top_right(value: impl Into<Pixels>) -> Radius { + Radius::default().top_right(value) +} + +/// Creates a new [`Radius`] with the given bottom right value. +pub fn bottom_right(value: impl Into<Pixels>) -> Radius { + Radius::default().bottom_right(value) +} + +/// Creates a new [`Radius`] with the given bottom left value. +pub fn bottom_left(value: impl Into<Pixels>) -> Radius { + Radius::default().bottom_left(value) +} + +impl Radius { + /// Creates a new [`Radius`] with the same value for each corner. + pub fn new(value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_left: value, + top_right: value, + bottom_right: value, + bottom_left: value, + } + } + + /// Sets the top left value of the [`Radius`]. + pub fn top_left(self, value: impl Into<Pixels>) -> Self { + Self { + top_left: value.into().0, + ..self + } + } + + /// Sets the top right value of the [`Radius`]. + pub fn top_right(self, value: impl Into<Pixels>) -> Self { + Self { + top_right: value.into().0, + ..self + } + } + + /// Sets the bottom right value of the [`Radius`]. + pub fn bottom_right(self, value: impl Into<Pixels>) -> Self { + Self { + bottom_right: value.into().0, + ..self + } + } + + /// Sets the bottom left value of the [`Radius`]. + pub fn bottom_left(self, value: impl Into<Pixels>) -> Self { + Self { + bottom_left: value.into().0, + ..self + } + } +} impl From<f32> for Radius { - fn from(w: f32) -> Self { - Self([w; 4]) + fn from(radius: f32) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_right: radius, + bottom_left: radius, + } } } @@ -80,14 +189,13 @@ impl From<i32> for Radius { } } -impl From<[f32; 4]> for Radius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - impl From<Radius> for [f32; 4] { fn from(radi: Radius) -> Self { - radi.0 + [ + radi.top_left, + radi.top_right, + radi.bottom_right, + radi.bottom_left, + ] } } diff --git a/core/src/padding.rs b/core/src/padding.rs index a0915fbc..fdaa0236 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -32,7 +32,7 @@ use crate::{Pixels, Size}; /// let widget = Widget::new().padding(20); // 20px on all sides /// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right /// ``` -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Default)] pub struct Padding { /// Top padding pub top: f32, @@ -51,34 +51,22 @@ pub fn all(padding: impl Into<Pixels>) -> Padding { /// Create some top [`Padding`]. pub fn top(padding: impl Into<Pixels>) -> Padding { - Padding { - top: padding.into().0, - ..Padding::ZERO - } + Padding::default().top(padding) } /// Create some bottom [`Padding`]. pub fn bottom(padding: impl Into<Pixels>) -> Padding { - Padding { - bottom: padding.into().0, - ..Padding::ZERO - } + Padding::default().bottom(padding) } /// Create some left [`Padding`]. pub fn left(padding: impl Into<Pixels>) -> Padding { - Padding { - left: padding.into().0, - ..Padding::ZERO - } + Padding::default().left(padding) } /// Create some right [`Padding`]. pub fn right(padding: impl Into<Pixels>) -> Padding { - Padding { - right: padding.into().0, - ..Padding::ZERO - } + Padding::default().right(padding) } impl Padding { diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 8f0c20aa..dc425cc6 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -3,12 +3,13 @@ mod quad { use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; + use iced::border; use iced::mouse; use iced::{Border, Color, Element, Length, Rectangle, Shadow, Size}; pub struct CustomQuad { size: f32, - radius: [f32; 4], + radius: border::Radius, border_width: f32, shadow: Shadow, } @@ -16,7 +17,7 @@ mod quad { impl CustomQuad { pub fn new( size: f32, - radius: [f32; 4], + radius: border::Radius, border_width: f32, shadow: Shadow, ) -> Self { @@ -63,7 +64,7 @@ mod quad { renderer::Quad { bounds: layout.bounds(), border: Border { - radius: self.radius.into(), + radius: self.radius, width: self.border_width, color: Color::from_rgb(1.0, 0.0, 0.0), }, @@ -81,6 +82,7 @@ mod quad { } } +use iced::border; use iced::widget::{center, column, slider, text}; use iced::{Center, Color, Element, Shadow, Vector}; @@ -89,7 +91,7 @@ pub fn main() -> iced::Result { } struct Example { - radius: [f32; 4], + radius: border::Radius, border_width: f32, shadow: Shadow, } @@ -110,7 +112,7 @@ enum Message { impl Example { fn new() -> Self { Self { - radius: [50.0; 4], + radius: border::radius(50), border_width: 0.0, shadow: Shadow { color: Color::from_rgba(0.0, 0.0, 0.0, 0.8), @@ -121,19 +123,18 @@ impl Example { } fn update(&mut self, message: Message) { - let [tl, tr, br, bl] = self.radius; match message { Message::RadiusTopLeftChanged(radius) => { - self.radius = [radius, tr, br, bl]; + self.radius = self.radius.top_left(radius); } Message::RadiusTopRightChanged(radius) => { - self.radius = [tl, radius, br, bl]; + self.radius = self.radius.top_right(radius); } Message::RadiusBottomRightChanged(radius) => { - self.radius = [tl, tr, radius, bl]; + self.radius = self.radius.bottom_right(radius); } Message::RadiusBottomLeftChanged(radius) => { - self.radius = [tl, tr, br, radius]; + self.radius = self.radius.bottom_left(radius); } Message::BorderWidthChanged(width) => { self.border_width = width; @@ -151,7 +152,13 @@ impl Example { } fn view(&self) -> Element<Message> { - let [tl, tr, br, bl] = self.radius; + let border::Radius { + top_left, + top_right, + bottom_right, + bottom_left, + } = self.radius; + let Shadow { offset: Vector { x: sx, y: sy }, blur_radius: sr, @@ -165,12 +172,12 @@ impl Example { self.border_width, self.shadow ), - text!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}"), - slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01), - slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01), - slider(1.0..=100.0, br, Message::RadiusBottomRightChanged) + text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"), + slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01), + slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01), + slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged) .step(0.01), - slider(1.0..=100.0, bl, Message::RadiusBottomLeftChanged) + slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged) .step(0.01), slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged) .step(0.01), diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 3b9b9d68..dc3f74ac 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -12,8 +12,9 @@ mod circle { use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; + use iced::border; use iced::mouse; - use iced::{Border, Color, Element, Length, Rectangle, Size}; + use iced::{Color, Element, Length, Rectangle, Size}; pub struct Circle { radius: f32, @@ -62,7 +63,7 @@ mod circle { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border: Border::rounded(self.radius), + border: border::rounded(self.radius), ..renderer::Quad::default() }, Color::BLACK, diff --git a/widget/src/button.rs b/widget/src/button.rs index fb505d6e..64a639d2 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,4 +1,5 @@ //! Allow your users to perform actions by pressing a button. +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -9,8 +10,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, - Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, + Shadow, Shell, Size, Theme, Vector, Widget, }; /// A generic widget that produces a message when pressed. @@ -591,7 +592,7 @@ fn styled(pair: palette::Pair) -> Style { Style { background: Some(Background::Color(pair.color)), text_color: pair.text, - border: Border::rounded(2), + border: border::rounded(2), ..Style::default() } } diff --git a/widget/src/container.rs b/widget/src/container.rs index 92b782e8..5680bc30 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -1,5 +1,6 @@ //! Decorate content and apply alignment. use crate::core::alignment::{self, Alignment}; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::gradient::{self, Gradient}; use crate::core::layout; @@ -9,9 +10,8 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, - Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, - Widget, + self, Background, Clipboard, Color, Element, Layout, Length, Padding, + Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; @@ -627,7 +627,7 @@ pub fn rounded_box(theme: &Theme) -> Style { Style { background: Some(palette.background.weak.color.into()), - border: Border::rounded(2), + border: border::rounded(2), ..Style::default() } } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index a43c8e8b..73d1cc8c 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -1,5 +1,6 @@ //! Build and show dropdown menus. use crate::core::alignment; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -9,8 +10,8 @@ use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Background, Border, Clipboard, Color, Length, Padding, Pixels, Point, - Rectangle, Size, Theme, Vector, + Background, Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, + Size, Theme, Vector, }; use crate::core::{Element, Shell, Widget}; use crate::scrollable::{self, Scrollable}; @@ -514,7 +515,7 @@ where width: bounds.width - style.border.width * 2.0, ..bounds }, - border: Border::rounded(style.border.radius), + border: border::rounded(style.border.radius), ..renderer::Quad::default() }, style.selected_background, diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index e7821b43..88d1850a 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -1,11 +1,11 @@ //! Provide progress feedback to your users. +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme, - Widget, + self, Background, Element, Layout, Length, Rectangle, Size, Theme, Widget, }; use std::ops::RangeInclusive; @@ -151,7 +151,7 @@ where width: active_progress_width, ..bounds }, - border: Border::rounded(style.border.radius), + border: border::rounded(style.border.radius), ..renderer::Quad::default() }, style.bar, @@ -255,6 +255,6 @@ fn styled( Style { background: background.into(), bar: bar.into(), - border: Border::rounded(2), + border: border::rounded(2), } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 6b22961d..ccc6a21e 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -1,5 +1,6 @@ //! Create choices using radio buttons. use crate::core::alignment; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -9,8 +10,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Rectangle, Shell, Size, Theme, Widget, + Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, + Shell, Size, Theme, Widget, }; /// A circular button representing a choice. @@ -342,7 +343,7 @@ where width: bounds.width - dot_size, height: bounds.height - dot_size, }, - border: Border::rounded(dot_size / 2.0), + border: border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, style.dot_color, diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 1a536d2f..bbcd577e 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,6 +1,6 @@ //! Display a horizontal or vertical rule for dividing content. use crate::core; -use crate::core::border::{self, Border}; +use crate::core::border; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -132,7 +132,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::rounded(style.radius), + border: border::rounded(style.radius), ..renderer::Quad::default() }, style.color, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cd669cad..e852f3ff 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,5 +1,6 @@ //! Navigate an endless amount of content with a scrollbar. use crate::container; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -11,8 +12,8 @@ use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, - Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, Background, Clipboard, Color, Element, Layout, Length, Padding, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -1767,10 +1768,10 @@ pub fn default(theme: &Theme, status: Status) -> Style { let scrollbar = Rail { background: Some(palette.background.weak.color.into()), - border: Border::rounded(2), + border: border::rounded(2), scroller: Scroller { color: palette.background.strong.color, - border: Border::rounded(2), + border: border::rounded(2), }, }; diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 74e6f8d3..b9419232 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -1,5 +1,5 @@ //! Display an interactive selector of a single value from a range of values. -use crate::core::border; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -9,8 +9,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Theme, Widget, + self, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, + Shell, Size, Theme, Widget, }; use std::ops::RangeInclusive; @@ -408,7 +408,7 @@ where width: offset + handle_width / 2.0, height: style.rail.width, }, - border: Border::rounded(style.rail.border_radius), + border: border::rounded(style.rail.border_radius), ..renderer::Quad::default() }, style.rail.colors.0, @@ -422,7 +422,7 @@ where width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border: Border::rounded(style.rail.border_radius), + border: border::rounded(style.rail.border_radius), ..renderer::Quad::default() }, style.rail.colors.1, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 33c591f5..6185295b 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -5,6 +5,7 @@ pub use crate::slider::{ default, Catalog, Handle, HandleShape, Status, Style, StyleFn, }; +use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -14,8 +15,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, - Size, Widget, + self, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size, + Widget, }; /// An vertical bar and a handle that selects a single value from a range of @@ -412,7 +413,7 @@ where width: style.rail.width, height: offset + handle_width / 2.0, }, - border: Border::rounded(style.rail.border_radius), + border: border::rounded(style.rail.border_radius), ..renderer::Quad::default() }, style.rail.colors.1, @@ -426,7 +427,7 @@ where width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border: Border::rounded(style.rail.border_radius), + border: border::rounded(style.rail.border_radius), ..renderer::Quad::default() }, style.rail.colors.0, From 2513213e8953fbaccb9ece1cabd3697cbca8aa2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 19:35:01 +0200 Subject: [PATCH 102/657] Add directional `border::Radius` helpers --- core/src/border.rs | 64 +++++++++++++++++++++++++++++++++++++++ core/src/border_radius.rs | 22 -------------- 2 files changed, 64 insertions(+), 22 deletions(-) delete mode 100644 core/src/border_radius.rs diff --git a/core/src/border.rs b/core/src/border.rs index 05e74ac6..da0aaa28 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -114,6 +114,26 @@ pub fn bottom_left(value: impl Into<Pixels>) -> Radius { Radius::default().bottom_left(value) } +/// Creates a new [`Radius`] with the given value as top left and top right. +pub fn top(value: impl Into<Pixels>) -> Radius { + Radius::default().top(value) +} + +/// Creates a new [`Radius`] with the given value as bottom left and bottom right. +pub fn bottom(value: impl Into<Pixels>) -> Radius { + Radius::default().bottom(value) +} + +/// Creates a new [`Radius`] with the given value as top left and bottom left. +pub fn left(value: impl Into<Pixels>) -> Radius { + Radius::default().left(value) +} + +/// Creates a new [`Radius`] with the given value as top right and bottom right. +pub fn right(value: impl Into<Pixels>) -> Radius { + Radius::default().right(value) +} + impl Radius { /// Creates a new [`Radius`] with the same value for each corner. pub fn new(value: impl Into<Pixels>) -> Self { @@ -158,6 +178,50 @@ impl Radius { ..self } } + + /// Sets the top left and top right values of the [`Radius`]. + pub fn top(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_left: value, + top_right: value, + ..self + } + } + + /// Sets the bottom left and bottom right values of the [`Radius`]. + pub fn bottom(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + bottom_left: value, + bottom_right: value, + ..self + } + } + + /// Sets the top left and bottom left values of the [`Radius`]. + pub fn left(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_left: value, + bottom_left: value, + ..self + } + } + + /// Sets the top right and bottom right values of the [`Radius`]. + pub fn right(self, value: impl Into<Pixels>) -> Self { + let value = value.into().0; + + Self { + top_right: value, + bottom_right: value, + ..self + } + } } impl From<f32> for Radius { diff --git a/core/src/border_radius.rs b/core/src/border_radius.rs deleted file mode 100644 index a444dd74..00000000 --- a/core/src/border_radius.rs +++ /dev/null @@ -1,22 +0,0 @@ -/// The border radii for the corners of a graphics primitive in the order: -/// top-left, top-right, bottom-right, bottom-left. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct BorderRadius([f32; 4]); - -impl From<f32> for BorderRadius { - fn from(w: f32) -> Self { - Self([w; 4]) - } -} - -impl From<[f32; 4]> for BorderRadius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - -impl From<BorderRadius> for [f32; 4] { - fn from(radi: BorderRadius) -> Self { - radi.0 - } -} From 3f480d3d18c41188bf40ead0a3dc4497316f11ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 12 Jul 2024 19:57:39 +0200 Subject: [PATCH 103/657] Rename `embed_*` in `Scrollable` to simply `spacing` --- examples/scrollable/src/main.rs | 14 +++-- widget/src/scrollable.rs | 99 +++++++++++++++++++-------------- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index de4f2f9a..969f385e 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -216,13 +216,14 @@ impl ScrollableDemo { .padding([40, 0]) .spacing(40), ) - .direction(scrollable::Direction::Vertical( - scrollable::Scrollbar::new() + .direction(scrollable::Direction::Vertical { + scrollbar: scrollable::Scrollbar::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) .anchor(self.anchor), - )) + spacing: None, + }) .width(Fill) .height(Fill) .id(SCROLLABLE_ID.clone()) @@ -242,13 +243,14 @@ impl ScrollableDemo { .padding([0, 40]) .spacing(40), ) - .direction(scrollable::Direction::Horizontal( - scrollable::Scrollbar::new() + .direction(scrollable::Direction::Horizontal { + scrollbar: scrollable::Scrollbar::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) .anchor(self.anchor), - )) + spacing: None, + }) .width(Fill) .height(Fill) .id(SCROLLABLE_ID.clone()) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e852f3ff..52e5391e 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -133,11 +133,14 @@ where /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable. pub fn anchor_x(mut self, alignment: Anchor) -> Self { match &mut self.direction { - Direction::Horizontal(horizontal) + Direction::Horizontal { + scrollbar: horizontal, + .. + } | Direction::Both { horizontal, .. } => { horizontal.alignment = alignment; } - Direction::Vertical(_) => {} + Direction::Vertical { .. } => {} } self @@ -146,37 +149,31 @@ where /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable. pub fn anchor_y(mut self, alignment: Anchor) -> Self { match &mut self.direction { - Direction::Vertical(vertical) + Direction::Vertical { + scrollbar: vertical, + .. + } | Direction::Both { vertical, .. } => { vertical.alignment = alignment; } - Direction::Horizontal(_) => {} + Direction::Horizontal { .. } => {} } self } - /// Sets whether the horizontal [`Scrollbar`] should be embedded in the [`Scrollable`]. - pub fn embed_x(mut self, embedded: bool) -> Self { + /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the + /// content. + /// + /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents + /// of the [`Scrollable`]. + pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self { match &mut self.direction { - Direction::Horizontal(horizontal) - | Direction::Both { horizontal, .. } => { - horizontal.embedded = embedded; + Direction::Horizontal { spacing, .. } + | Direction::Vertical { spacing, .. } => { + *spacing = Some(new_spacing.into().0); } - Direction::Vertical(_) => {} - } - - self - } - - /// Sets whether the vertical [`Scrollbar`] should be embedded in the [`Scrollable`]. - pub fn embed_y(mut self, embedded: bool) -> Self { - match &mut self.direction { - Direction::Vertical(vertical) - | Direction::Both { vertical, .. } => { - vertical.embedded = embedded; - } - Direction::Horizontal(_) => {} + Direction::Both { .. } => {} } self @@ -205,9 +202,19 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub enum Direction { /// Vertical scrolling - Vertical(Scrollbar), + Vertical { + /// The vertical [`Scrollbar`]. + scrollbar: Scrollbar, + /// The amount of spacing between the [`Scrollbar`] and the contents, if embedded. + spacing: Option<f32>, + }, /// Horizontal scrolling - Horizontal(Scrollbar), + Horizontal { + /// The horizontal [`Scrollbar`]. + scrollbar: Scrollbar, + /// The amount of spacing between the [`Scrollbar`] and the contents, if embedded. + spacing: Option<f32>, + }, /// Both vertical and horizontal scrolling Both { /// The properties of the vertical scrollbar. @@ -221,25 +228,28 @@ impl Direction { /// Returns the horizontal [`Scrollbar`], if any. pub fn horizontal(&self) -> Option<&Scrollbar> { match self { - Self::Horizontal(properties) => Some(properties), + Self::Horizontal { scrollbar, .. } => Some(scrollbar), Self::Both { horizontal, .. } => Some(horizontal), - Self::Vertical(_) => None, + Self::Vertical { .. } => None, } } /// Returns the vertical [`Scrollbar`], if any. pub fn vertical(&self) -> Option<&Scrollbar> { match self { - Self::Vertical(properties) => Some(properties), + Self::Vertical { scrollbar, .. } => Some(scrollbar), Self::Both { vertical, .. } => Some(vertical), - Self::Horizontal(_) => None, + Self::Horizontal { .. } => None, } } } impl Default for Direction { fn default() -> Self { - Self::Vertical(Scrollbar::default()) + Self::Vertical { + scrollbar: Scrollbar::default(), + spacing: None, + } } } @@ -250,7 +260,7 @@ pub struct Scrollbar { margin: f32, scroller_width: f32, alignment: Anchor, - embedded: bool, + spacing: Option<f32>, } impl Default for Scrollbar { @@ -260,7 +270,7 @@ impl Default for Scrollbar { margin: 0.0, scroller_width: 10.0, alignment: Anchor::Start, - embedded: false, + spacing: None, } } } @@ -295,12 +305,13 @@ impl Scrollbar { self } - /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`]. + /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using + /// the given spacing between itself and the contents. /// /// An embedded [`Scrollbar`] will always be displayed, will take layout space, /// and will not float over the contents. - pub fn embedded(mut self, embedded: bool) -> Self { - self.embedded = embedded; + pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { + self.spacing = Some(spacing.into().0); self } } @@ -352,12 +363,14 @@ where limits: &layout::Limits, ) -> layout::Node { let (right_padding, bottom_padding) = match self.direction { - Direction::Vertical(scrollbar) if scrollbar.embedded => { - (scrollbar.width + scrollbar.margin * 2.0, 0.0) - } - Direction::Horizontal(scrollbar) if scrollbar.embedded => { - (0.0, scrollbar.width + scrollbar.margin * 2.0) - } + Direction::Vertical { + scrollbar, + spacing: Some(spacing), + } => (scrollbar.width + scrollbar.margin * 2.0 + spacing, 0.0), + Direction::Horizontal { + scrollbar, + spacing: Some(spacing), + } => (0.0, scrollbar.width + scrollbar.margin * 2.0 + spacing), _ => (0.0, 0.0), }; @@ -1407,11 +1420,11 @@ impl Scrollbars { let translation = state.translation(direction, bounds, content_bounds); let show_scrollbar_x = direction.horizontal().filter(|scrollbar| { - scrollbar.embedded || content_bounds.width > bounds.width + scrollbar.spacing.is_some() || content_bounds.width > bounds.width }); let show_scrollbar_y = direction.vertical().filter(|scrollbar| { - scrollbar.embedded || content_bounds.height > bounds.height + scrollbar.spacing.is_some() || content_bounds.height > bounds.height }); let y_scrollbar = if let Some(vertical) = show_scrollbar_y { From 1eabd3821928f47451363f7bca3757701182a4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 13 Jul 2024 00:19:33 +0200 Subject: [PATCH 104/657] Set default `width` of `toggler` widget to `Shrink` --- widget/src/toggler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index ca6e37b0..853d27ac 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -80,7 +80,7 @@ where is_toggled, on_toggle: Box::new(f), label: label.into(), - width: Length::Fill, + width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, text_line_height: text::LineHeight::default(), From a108b2eebe210c774d07e436be5d73293dfea9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 13 Jul 2024 12:53:06 +0200 Subject: [PATCH 105/657] Add `resize_events` subscription to `window` module --- core/src/window/event.rs | 14 ++------------ runtime/src/window.rs | 11 +++++++++++ winit/src/conversion.rs | 6 +++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/core/src/window/event.rs b/core/src/window/event.rs index a14d127f..c9532e0d 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -23,20 +23,10 @@ pub enum Event { Closed, /// A window was moved. - Moved { - /// The new logical x location of the window - x: i32, - /// The new logical y location of the window - y: i32, - }, + Moved(Point), /// A window was resized. - Resized { - /// The new logical width of the window - width: u32, - /// The new logical height of the window - height: u32, - }, + Resized(Size), /// A window redraw was requested. /// diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 815827d1..ee03f84f 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -194,6 +194,17 @@ pub fn close_events() -> Subscription<Id> { }) } +/// Subscribes to all [`Event::Resized`] occurrences in the running application. +pub fn resize_events() -> Subscription<(Id, Size)> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Resized(size)) = event { + Some((id, size)) + } else { + None + } + }) +} + /// Subscribes to all [`Event::CloseRequested`] occurences in the running application. pub fn close_requests() -> Subscription<Id> { event::listen_with(|event, _status, id| { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0ed10c88..e88ff84d 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -132,10 +132,10 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { + Some(Event::Window(window::Event::Resized(Size { width: logical_size.width, height: logical_size.height, - })) + }))) } WindowEvent::CloseRequested => { Some(Event::Window(window::Event::CloseRequested)) @@ -277,7 +277,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(window::Event::Moved { x, y })) + Some(Event::Window(window::Event::Moved(Point::new(x, y)))) } _ => None, } From 5e6d99419906077e75268ba136542254611df9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 13 Jul 2024 13:26:22 +0200 Subject: [PATCH 106/657] Add `default` and `base` stylings to `text` widget --- core/src/widget/text.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 990c5567..91c9893d 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -373,6 +373,18 @@ impl Catalog for Theme { } } +/// The default text styling; color is inherited. +pub fn default(_theme: &Theme) -> Style { + Style { color: None } +} + +/// Text with the default base color. +pub fn base(theme: &Theme) -> Style { + Style { + color: Some(theme.palette().text), + } +} + /// Text conveying some important information, like an action. pub fn primary(theme: &Theme) -> Style { Style { From d9a29f51760efc0b2a9d3b0947c15c51897a7a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 13 Jul 2024 13:41:00 +0200 Subject: [PATCH 107/657] Remove `Vector::UNIT` constant --- core/src/vector.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/vector.rs b/core/src/vector.rs index 049e648f..1380c3b3 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,9 +18,6 @@ impl<T> Vector<T> { impl Vector { /// The zero [`Vector`]. pub const ZERO: Self = Self::new(0.0, 0.0); - - /// The unit [`Vector`]. - pub const UNIT: Self = Self::new(0.0, 0.0); } impl<T> std::ops::Add for Vector<T> From fd0abe18d0cfde614cc779fa0da71c4e07107b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 14 Jul 2024 22:51:52 +0200 Subject: [PATCH 108/657] Implement `application::Update` for `()` --- runtime/src/task.rs | 5 +---- src/application.rs | 10 ++++++++++ src/lib.rs | 2 -- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 72f408e0..4d75ddaa 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -283,10 +283,7 @@ impl<T, E> Task<Result<T, E>> { } } -impl<T> From<()> for Task<T> -where - T: MaybeSend + 'static, -{ +impl<T> From<()> for Task<T> { fn from(_value: ()) -> Self { Self::none() } diff --git a/src/application.rs b/src/application.rs index f5e06471..71cb6a7f 100644 --- a/src/application.rs +++ b/src/application.rs @@ -417,6 +417,16 @@ pub trait Update<State, Message> { ) -> impl Into<Task<Message>>; } +impl<State, Message> Update<State, Message> for () { + fn update( + &self, + _state: &mut State, + _message: Message, + ) -> impl Into<Task<Message>> { + () + } +} + impl<T, State, Message, C> Update<State, Message> for T where T: Fn(&mut State, Message) -> C, diff --git a/src/lib.rs b/src/lib.rs index 7ced9a57..138f0b04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -345,8 +345,6 @@ pub type Result = std::result::Result<(), Error>; /// /// This is equivalent to chaining [`application()`] with [`Application::run`]. /// -/// [`program`]: program() -/// /// # Example /// ```no_run /// use iced::widget::{button, column, text, Column}; From 950bfc07d4b71016bf3e9d53709395e185420cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 14 Jul 2024 22:58:30 +0200 Subject: [PATCH 109/657] Export `operate` constructor in `advanced::widget` --- src/advanced.rs | 22 ++++++++++++++-------- src/application.rs | 1 - 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/advanced.rs b/src/advanced.rs index b817bbf9..57e40de6 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,4 +1,17 @@ //! Leverage advanced concepts like custom widgets. +pub mod subscription { + //! Write your own subscriptions. + pub use crate::runtime::futures::subscription::{ + from_recipe, into_recipes, EventStream, Hasher, Recipe, + }; +} + +pub mod widget { + //! Create custom widgets and operate on them. + pub use crate::core::widget::*; + pub use crate::runtime::task::widget as operate; +} + pub use crate::core::clipboard::{self, Clipboard}; pub use crate::core::image; pub use crate::core::layout::{self, Layout}; @@ -7,13 +20,6 @@ pub use crate::core::overlay::{self, Overlay}; pub use crate::core::renderer::{self, Renderer}; pub use crate::core::svg; pub use crate::core::text::{self, Text}; -pub use crate::core::widget::{self, Widget}; pub use crate::core::Shell; pub use crate::renderer::graphics; - -pub mod subscription { - //! Write your own subscriptions. - pub use crate::runtime::futures::subscription::{ - from_recipe, into_recipes, EventStream, Hasher, Recipe, - }; -} +pub use widget::Widget; diff --git a/src/application.rs b/src/application.rs index 71cb6a7f..c21f343a 100644 --- a/src/application.rs +++ b/src/application.rs @@ -423,7 +423,6 @@ impl<State, Message> Update<State, Message> for () { _state: &mut State, _message: Message, ) -> impl Into<Task<Message>> { - () } } From bdf0430880f5c29443f5f0a0ae4895866dfef4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 15 Jul 2024 13:34:22 +0200 Subject: [PATCH 110/657] Make `run_with` take a `FnOnce` --- src/application.rs | 2 +- src/daemon.rs | 2 +- src/program.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/application.rs b/src/application.rs index c21f343a..d0f77304 100644 --- a/src/application.rs +++ b/src/application.rs @@ -169,7 +169,7 @@ impl<P: Program> Application<P> { pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> (P::State, Task<P::Message>) + Clone + 'static, + I: FnOnce() -> (P::State, Task<P::Message>) + 'static, { self.raw .run_with(self.settings, Some(self.window), initialize) diff --git a/src/daemon.rs b/src/daemon.rs index d2de2db7..6a6ad133 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -119,7 +119,7 @@ impl<P: Program> Daemon<P> { pub fn run_with<I>(self, initialize: I) -> Result where Self: 'static, - I: Fn() -> (P::State, Task<P::Message>) + Clone + 'static, + I: FnOnce() -> (P::State, Task<P::Message>) + 'static, { self.raw.run_with(self.settings, None, initialize) } diff --git a/src/program.rs b/src/program.rs index 939b0047..2b697fbe 100644 --- a/src/program.rs +++ b/src/program.rs @@ -92,7 +92,7 @@ pub trait Program: Sized { ) -> Result where Self: 'static, - I: Fn() -> (Self::State, Task<Self::Message>) + Clone + 'static, + I: FnOnce() -> (Self::State, Task<Self::Message>) + 'static, { use std::marker::PhantomData; @@ -102,8 +102,8 @@ pub trait Program: Sized { _initialize: PhantomData<I>, } - impl<P: Program, I: Fn() -> (P::State, Task<P::Message>)> shell::Program - for Instance<P, I> + impl<P: Program, I: FnOnce() -> (P::State, Task<P::Message>)> + shell::Program for Instance<P, I> { type Message = P::Message; type Theme = P::Theme; From 143f4c86caeb43cfff6573fe192c8eb877bb044c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 16 Jul 2024 01:14:26 +0200 Subject: [PATCH 111/657] Draft "The Pocket Guide" for the API reference --- src/lib.rs | 405 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 281 insertions(+), 124 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 138f0b04..0bb8fc3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,170 +1,327 @@ -//! Iced is a cross-platform GUI library focused on simplicity and type-safety. +//! iced is a cross-platform GUI library focused on simplicity and type-safety. //! Inspired by [Elm]. //! -//! # Features -//! * Simple, easy-to-use, batteries-included API -//! * Type-safe, reactive programming model -//! * [Cross-platform support] (Windows, macOS, Linux, and the Web) -//! * Responsive layout -//! * Built-in widgets (including [text inputs], [scrollables], and more!) -//! * Custom widget support (create your own!) -//! * [Debug overlay with performance metrics] -//! * First-class support for async actions (use futures!) -//! * [Modular ecosystem] split into reusable parts: -//! * A [renderer-agnostic native runtime] enabling integration with existing -//! systems -//! * A [built-in renderer] supporting Vulkan, Metal, DX11, and DX12 -//! * A [windowing shell] -//! * A [web runtime] leveraging the DOM +//! # Disclaimer +//! iced is __experimental__ software. If you expect the documentation to hold your hand +//! as you learn the ropes, you are in for a frustrating experience. //! -//! Check out the [repository] and the [examples] for more details! +//! The library leverages Rust to its full extent: ownership, borrowing, lifetimes, futures, +//! streams, first-class functions, trait bounds, closures, and more. This documentation +//! is not meant to teach you any of these. Far from it, it will assume you have __mastered__ +//! all of them. //! -//! [Cross-platform support]: https://github.com/iced-rs/iced/blob/master/docs/images/todos_desktop.jpg?raw=true -//! [text inputs]: https://iced.rs/examples/text_input.mp4 -//! [scrollables]: https://iced.rs/examples/scrollable.mp4 -//! [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4 -//! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md -//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.12/runtime -//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.12/wgpu -//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.12/winit -//! [`dodrio`]: https://github.com/fitzgen/dodrio -//! [web runtime]: https://github.com/iced-rs/iced_web -//! [examples]: https://github.com/iced-rs/iced/tree/0.12/examples -//! [repository]: https://github.com/iced-rs/iced +//! Furthermore—just like Rust—iced is very unforgiving. It will not let you easily cut corners. +//! The type signatures alone can be used to learn how to use most of the library. +//! Everything is connected. //! -//! # Overview -//! Inspired by [The Elm Architecture], Iced expects you to split user -//! interfaces into four different concepts: +//! Therefore, iced is easy to learn for __advanced__ Rust programmers; but plenty of patient +//! beginners have learned it and had a good time with it. Since it leverages a lot of what +//! Rust has to offer in a type-safe way, it can be a great way to discover Rust itself. //! -//! * __State__ — the state of your application -//! * __Messages__ — user interactions or meaningful events that you care -//! about -//! * __View logic__ — a way to display your __state__ as widgets that -//! may produce __messages__ on user interaction -//! * __Update logic__ — a way to react to __messages__ and update your -//! __state__ +//! If you don't like the sound of that, you expect to be spoonfed, or you feel frustrated +//! and struggle to use the library; then I recommend you to wait patiently until [the book] +//! is finished. //! -//! We can build something to see how this works! Let's say we want a simple -//! counter that can be incremented and decremented using two buttons. +//! [the book]: https://book.iced.rs //! -//! We start by modelling the __state__ of our application: +//! # The Pocket Guide +//! Start by calling [`run`]: //! +//! ```rust,no_run +//! pub fn main() -> iced::Result { +//! iced::run("A cool counter", update, view) +//! } +//! # fn update(state: &mut (), message: ()) {} +//! # fn view(state: &()) -> iced::Element<()> { iced::widget::text("").into() } //! ``` +//! +//! Define an `update` function to __change__ your state: +//! +//! ```rust +//! fn update(counter: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *counter += 1, +//! } +//! } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! ``` +//! +//! Define a `view` function to __display__ your state: +//! +//! ```rust +//! use iced::widget::{button, text}; +//! use iced::Element; +//! +//! fn view(counter: &u64) -> Element<Message> { +//! button(text(counter)).on_press(Message::Increment).into() +//! } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! ``` +//! +//! And create a `Message` enum to __connect__ `view` and `update` together: +//! +//! ```rust +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! ``` +//! +//! ## Custom State +//! You can define your own struct for your state: +//! +//! ```rust //! #[derive(Default)] //! struct Counter { -//! // The counter value -//! value: i32, +//! value: u64, //! } //! ``` //! -//! Next, we need to define the possible user interactions of our counter: -//! the button presses. These interactions are our __messages__: +//! But you have to change `update` and `view` accordingly: //! -//! ``` -//! #[derive(Debug, Clone, Copy)] -//! pub enum Message { -//! Increment, -//! Decrement, +//! ```rust +//! # struct Counter { value: u64 } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! # use iced::widget::{button, text}; +//! # use iced::Element; +//! fn update(counter: &mut Counter, message: Message) { +//! match message { +//! Message::Increment => counter.value += 1, +//! } +//! } +//! +//! fn view(counter: &Counter) -> Element<Message> { +//! button(text(counter.value)).on_press(Message::Increment).into() //! } //! ``` //! -//! Now, let's show the actual counter by putting it all together in our -//! __view logic__: +//! ## Widgets and Elements +//! The `view` function must return an [`Element`]. An [`Element`] is just a generic [`widget`]. //! +//! The [`widget`] module contains a bunch of functions to help you build +//! and use widgets. +//! +//! Widgets are configured using the builder pattern: +//! +//! ```rust +//! # struct Counter { value: u64 } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! use iced::widget::{button, column, text}; +//! use iced::Element; +//! +//! fn view(counter: &Counter) -> Element<Message> { +//! column![ +//! text(counter.value).size(20), +//! button("Increment").on_press(Message::Increment), +//! ] +//! .spacing(10) +//! .into() +//! } //! ``` -//! # struct Counter { -//! # // The counter value -//! # value: i32, -//! # } -//! # -//! # #[derive(Debug, Clone, Copy)] -//! # pub enum Message { -//! # Increment, -//! # Decrement, -//! # } -//! # -//! use iced::widget::{button, column, text, Column}; //! -//! impl Counter { -//! pub fn view(&self) -> Column<Message> { -//! // We use a column: a simple vertical layout +//! A widget can be turned into an [`Element`] by calling `into`. +//! +//! Widgets and elements are generic over the message type they produce. The +//! [`Element`] returned by `view` must have the same `Message` type as +//! your `update`. +//! +//! ## Layout +//! There is no unified layout system in iced. Instead, each widget implements +//! its own layout strategy. +//! +//! Generally, building your layout will consist in using a combination of +//! [rows], [columns], and [containers]: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::{column, container, row}; +//! use iced::{Fill, Element}; +//! +//! fn view(state: &State) -> Element<Message> { +//! container( //! column![ -//! // The increment button. We tell it to produce an -//! // `Increment` message when pressed -//! button("+").on_press(Message::Increment), -//! -//! // We show the value of the counter here -//! text(self.value).size(50), -//! -//! // The decrement button. We tell it to produce a -//! // `Decrement` message when pressed -//! button("-").on_press(Message::Decrement), +//! "Top", +//! row!["Left", "Right"].spacing(10), +//! "Bottom" //! ] -//! } +//! .spacing(10) +//! ) +//! .padding(10) +//! .center_x(Fill) +//! .center_y(Fill) +//! .into() //! } //! ``` //! -//! Finally, we need to be able to react to any produced __messages__ and change -//! our __state__ accordingly in our __update logic__: +//! Rows and columns lay out their children horizontally and vertically, +//! respectively. [Spacing] can be easily added between elements. //! -//! ``` -//! # struct Counter { -//! # // The counter value -//! # value: i32, -//! # } -//! # -//! # #[derive(Debug, Clone, Copy)] -//! # pub enum Message { -//! # Increment, -//! # Decrement, -//! # } -//! impl Counter { -//! // ... +//! Containers position or align a single widget inside their bounds. //! -//! pub fn update(&mut self, message: Message) { -//! match message { -//! Message::Increment => { -//! self.value += 1; -//! } -//! Message::Decrement => { -//! self.value -= 1; -//! } -//! } -//! } +//! [rows]: widget::Row +//! [columns]: widget::Column +//! [containers]: widget::Container +//! [Spacing]: widget::Column::spacing +//! +//! ## Sizing +//! The width and height of widgets can generally be defined using a [`Length`]. +//! +//! - [`Fill`] will make the widget take all the available space in a given axis. +//! - [`Shrink`] will make the widget use its intrinsic size. +//! +//! Most widgets use a [`Shrink`] sizing strategy by default, but will inherit +//! a [`Fill`] strategy from their children. +//! +//! A fixed numeric [`Length`] in [`Pixels`] can also be used: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::container; +//! use iced::Element; +//! +//! fn view(state: &State) -> Element<Message> { +//! container("I am 300px tall!").height(300).into() //! } //! ``` //! -//! And that's everything! We just wrote a whole user interface. Let's run it: +//! ## Theming +//! The default [`Theme`] of an application can be changed by defining a `theme` +//! function and leveraging the [`Application`] builder, instead of directly +//! calling [`run`]: //! -//! ```no_run +//! ```rust,no_run //! # #[derive(Default)] -//! # struct Counter; -//! # impl Counter { -//! # fn update(&mut self, _message: ()) {} -//! # fn view(&self) -> iced::Element<()> { unimplemented!() } -//! # } -//! # -//! fn main() -> iced::Result { -//! iced::run("A cool counter", Counter::update, Counter::view) +//! # struct State; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::application("A cool application", update, view) +//! .theme(theme) +//! .run() +//! } +//! +//! fn theme(state: &State) -> Theme { +//! Theme::TokyoNight +//! } +//! # fn update(state: &mut State, message: ()) {} +//! # fn view(state: &State) -> iced::Element<()> { iced::widget::text("").into() } +//! ``` +//! +//! The `theme` function takes the current state of the application, allowing the +//! returned [`Theme`] to be completely dynamic—just like `view`. +//! +//! There are a bunch of built-in [`Theme`] variants at your disposal, but you can +//! also [create your own](Theme::custom). +//! +//! ## Styling +//! As with layout, iced does not have a unified styling system. However, all +//! of the built-in widgets follow the same styling approach. +//! +//! The appearance of a widget can be changed by calling its `style` method: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::container; +//! use iced::Element; +//! +//! fn view(state: &State) -> Element<Message> { +//! container("I am a rounded box!").style(container::rounded_box).into() //! } //! ``` //! -//! Iced will automatically: +//! The `style` method of a widget takes a closure that, given the current active +//! [`Theme`], returns the widget style: //! -//! 1. Take the result of our __view logic__ and layout its widgets. -//! 1. Process events from our system and produce __messages__ for our -//! __update logic__. -//! 1. Draw the resulting user interface. +//! ```rust +//! # struct State; +//! # #[derive(Clone)] +//! # enum Message {} +//! use iced::widget::button; +//! use iced::{Element, Theme}; //! -//! # Usage -//! Use [`run`] or the [`application`] builder. +//! fn view(state: &State) -> Element<Message> { +//! button("I am a styled button!").style(|theme: &Theme, status| { +//! let palette = theme.extended_palette(); +//! +//! match status { +//! button::Status::Active => { +//! button::Style::default() +//! .with_background(palette.success.strong.color) +//! } +//! _ => button::primary(theme, status), +//! } +//! }) +//! .into() +//! } +//! ``` +//! +//! Widgets that can be in multiple different states will also provide the closure +//! with some [`Status`], allowing you to use a different style for each state. +//! +//! You can extract the [`Palette`] colors of a [`Theme`] with the [`palette`] or +//! [`extended_palette`] methods. +//! +//! Most widgets provide styling functions for your convenience in their respective modules; +//! like [`container::rounded_box`], [`button::primary`], or [`text::danger`]. +//! +//! [`Status`]: widget::button::Status +//! [`palette`]: Theme::palette +//! [`extended_palette`]: Theme::extended_palette +//! [`container::rounded_box`]: widget::container::rounded_box +//! [`button::primary`]: widget::button::primary +//! [`text::danger`]: widget::text::danger +//! +//! ## Concurrent Tasks +//! The `update` function can _optionally_ return a [`Task`]. +//! +//! A [`Task`] can be leveraged to perform asynchronous work, like running a +//! future or a stream: +//! +//! ```rust +//! # #[derive(Clone)] +//! # struct Weather; +//! use iced::Task; +//! +//! struct State { +//! weather: Option<Weather>, +//! } +//! +//! enum Message { +//! FetchWeather, +//! WeatherFetched(Weather), +//! } +//! +//! fn update(state: &mut State, message: Message) -> Task<Message> { +//! match message { +//! Message::FetchWeather => Task::perform( +//! fetch_weather(), +//! Message::WeatherFetched, +//! ), +//! Message::WeatherFetched(weather) => { +//! state.weather = Some(weather); +//! +//! Task::none() +//! } +//! } +//! } +//! +//! async fn fetch_weather() -> Weather { +//! // ... +//! # unimplemented!() +//! } +//! ``` //! //! [Elm]: https://elm-lang.org/ -//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ //! [`application`]: application() #![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/bdf0430880f5c29443f5f0a0ae4895866dfef4c6/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] From eb6673bf0022d09f86d0ba0b8c3313abb95eeb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 16 Jul 2024 15:41:28 +0200 Subject: [PATCH 112/657] Finish "The Pocket Guide" --- futures/src/keyboard.rs | 4 +- src/lib.rs | 131 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index f0d7d757..35f6b6fa 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -6,7 +6,7 @@ use crate::subscription::{self, Subscription}; use crate::MaybeSend; /// Listens to keyboard key presses and calls the given function -/// map them into actual messages. +/// to map them into actual messages. /// /// If the function returns `None`, the key press will be simply /// ignored. @@ -31,7 +31,7 @@ where } /// Listens to keyboard key releases and calls the given function -/// map them into actual messages. +/// to map them into actual messages. /// /// If the function returns `None`, the key release will be simply /// ignored. diff --git a/src/lib.rs b/src/lib.rs index 0bb8fc3b..82d6bc50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ //! iced is a cross-platform GUI library focused on simplicity and type-safety. //! Inspired by [Elm]. //! +//! [Elm]: https://elm-lang.org/ +//! //! # Disclaimer //! iced is __experimental__ software. If you expect the documentation to hold your hand //! as you learn the ropes, you are in for a frustrating experience. @@ -133,7 +135,7 @@ //! There is no unified layout system in iced. Instead, each widget implements //! its own layout strategy. //! -//! Generally, building your layout will consist in using a combination of +//! Building your layout will often consist in using a combination of //! [rows], [columns], and [containers]: //! //! ```rust @@ -318,8 +320,131 @@ //! } //! ``` //! -//! [Elm]: https://elm-lang.org/ -//! [`application`]: application() +//! Tasks can also be used to interact with the iced runtime. Some modules +//! expose functions that create tasks for different purposes—like [changing +//! window settings](window#functions), [focusing a widget](widget::focus_next), or +//! [querying its visible bounds](widget::container::visible_bounds). +//! +//! Like futures and streams, tasks expose [a monadic interface](Task::then)—but they can also be +//! [mapped](Task::map), [chained](Task::chain), [batched](Task::batch), [canceled](Task::abortable), +//! and more. +//! +//! ## Passive Subscriptions +//! Applications can subscribe to passive sources of data—like time ticks or runtime events. +//! +//! You will need to define a `subscription` function and use the [`Application`] builder: +//! +//! ```rust,no_run +//! # #[derive(Default)] +//! # struct State; +//! use iced::window; +//! use iced::{Size, Subscription}; +//! +//! #[derive(Debug)] +//! enum Message { +//! WindowResized(Size), +//! } +//! +//! pub fn main() -> iced::Result { +//! iced::application("A cool application", update, view) +//! .subscription(subscription) +//! .run() +//! } +//! +//! fn subscription(state: &State) -> Subscription<Message> { +//! window::resize_events().map(|(_id, size)| Message::WindowResized(size)) +//! } +//! # fn update(state: &mut State, message: Message) {} +//! # 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) +//! that are not allowed to end on its own. Only the `subscription` function +//! dictates the active subscriptions—just like `view` fully dictates the +//! visible widgets of your user interface, at every moment. +//! +//! As with tasks, some modules expose convenient functions that build a [`Subscription`] for you—like +//! [`time::every`] which can be used to listen to time, or [`keyboard::on_key_press`] which will notify you +//! of any key presses. But you can also create your own with [`Subscription::run`] and [`run_with_id`]. +//! +//! [`run_with_id`]: Subscription::run_with_id +//! +//! ## Scaling Applications +//! The `update`, `view`, and `Message` triplet composes very nicely. +//! +//! A common pattern is to leverage this composability to split an +//! application into different screens: +//! +//! ```rust +//! # mod contacts { +//! # use iced::{Element, Task}; +//! # pub struct Contacts; +//! # impl Contacts { +//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() } +//! # pub fn view(&self) -> Element<Message> { unimplemented!() } +//! # } +//! # #[derive(Debug)] +//! # pub enum Message {} +//! # } +//! # mod conversation { +//! # use iced::{Element, Task}; +//! # pub struct Conversation; +//! # impl Conversation { +//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() } +//! # pub fn view(&self) -> Element<Message> { unimplemented!() } +//! # } +//! # #[derive(Debug)] +//! # pub enum Message {} +//! # } +//! use contacts::Contacts; +//! use conversation::Conversation; +//! +//! use iced::{Element, Task}; +//! +//! struct State { +//! screen: Screen, +//! } +//! +//! enum Screen { +//! Contacts(Contacts), +//! Conversation(Conversation), +//! } +//! +//! #[derive(Debug)] +//! enum Message { +//! Contacts(contacts::Message), +//! Conversation(conversation::Message) +//! } +//! +//! fn update(state: &mut State, message: Message) -> Task<Message> { +//! match message { +//! Message::Contacts(message) => { +//! if let Screen::Contacts(contacts) = &mut state.screen { +//! contacts.update(message).map(Message::Contacts) +//! } else { +//! Task::none() +//! } +//! } +//! Message::Conversation(message) => { +//! if let Screen::Conversation(conversation) = &mut state.screen { +//! conversation.update(message).map(Message::Conversation) +//! } else { +//! Task::none() +//! } +//! } +//! } +//! } +//! +//! 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), +//! } +//! } +//! ``` +//! +//! Functor methods like [`Task::map`], [`Element::map`], and [`Subscription::map`] make this +//! approach seamless. #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/bdf0430880f5c29443f5f0a0ae4895866dfef4c6/docs/logo.svg" )] From 964182e4b8c8e4e4f1afed67d371d642496fce83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 16 Jul 2024 16:12:23 +0200 Subject: [PATCH 113/657] Fix grammar in "Passive Subscriptions" docs --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 82d6bc50..a240912c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -359,7 +359,7 @@ //! ``` //! //! A [`Subscription`] is [a _declarative_ builder of streams](Subscription#the-lifetime-of-a-subscription) -//! that are not allowed to end on its own. Only the `subscription` function +//! that are not allowed to end on their own. Only the `subscription` function //! dictates the active subscriptions—just like `view` fully dictates the //! visible widgets of your user interface, at every moment. //! From f0036400a10e6df30f149d1bd16c5ec495abb5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 16 Jul 2024 16:15:43 +0200 Subject: [PATCH 114/657] Remove unnecessary `derive` in "Scaling Applications" docs --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a240912c..022f8d6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -410,7 +410,6 @@ //! Conversation(Conversation), //! } //! -//! #[derive(Debug)] //! enum Message { //! Contacts(contacts::Message), //! Conversation(conversation::Message) From 24f74768236dac874a602f7aa7de4ca8a0cc793f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 16 Jul 2024 16:34:29 +0200 Subject: [PATCH 115/657] Fix broken `futures-core` links in nightly docs --- .github/workflows/document.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index a213e590..57dc1375 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -14,6 +14,7 @@ jobs: run: | RUSTDOCFLAGS="--cfg docsrs" \ cargo doc --no-deps --all-features \ + -p futures-core \ -p iced_core \ -p iced_highlighter \ -p iced_futures \ From b518e30610fa53691c727852f70b497dd19cfc7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 16 Jul 2024 19:05:46 +0200 Subject: [PATCH 116/657] Fix `Scrollable::spacing` not embedding the `Scrollbar` --- examples/scrollable/src/main.rs | 14 ++++---- widget/src/scrollable.rs | 59 ++++++++++++--------------------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 969f385e..de4f2f9a 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -216,14 +216,13 @@ impl ScrollableDemo { .padding([40, 0]) .spacing(40), ) - .direction(scrollable::Direction::Vertical { - scrollbar: scrollable::Scrollbar::new() + .direction(scrollable::Direction::Vertical( + scrollable::Scrollbar::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) .anchor(self.anchor), - spacing: None, - }) + )) .width(Fill) .height(Fill) .id(SCROLLABLE_ID.clone()) @@ -243,14 +242,13 @@ impl ScrollableDemo { .padding([0, 40]) .spacing(40), ) - .direction(scrollable::Direction::Horizontal { - scrollbar: scrollable::Scrollbar::new() + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new() .width(self.scrollbar_width) .margin(self.scrollbar_margin) .scroller_width(self.scroller_width) .anchor(self.anchor), - spacing: None, - }) + )) .width(Fill) .height(Fill) .id(SCROLLABLE_ID.clone()) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 52e5391e..b1082203 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -133,10 +133,7 @@ where /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable. pub fn anchor_x(mut self, alignment: Anchor) -> Self { match &mut self.direction { - Direction::Horizontal { - scrollbar: horizontal, - .. - } + Direction::Horizontal(horizontal) | Direction::Both { horizontal, .. } => { horizontal.alignment = alignment; } @@ -149,10 +146,7 @@ where /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable. pub fn anchor_y(mut self, alignment: Anchor) -> Self { match &mut self.direction { - Direction::Vertical { - scrollbar: vertical, - .. - } + Direction::Vertical(vertical) | Direction::Both { vertical, .. } => { vertical.alignment = alignment; } @@ -169,9 +163,9 @@ where /// of the [`Scrollable`]. pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self { match &mut self.direction { - Direction::Horizontal { spacing, .. } - | Direction::Vertical { spacing, .. } => { - *spacing = Some(new_spacing.into().0); + Direction::Horizontal(scrollbar) + | Direction::Vertical(scrollbar) => { + scrollbar.spacing = Some(new_spacing.into().0); } Direction::Both { .. } => {} } @@ -202,19 +196,9 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub enum Direction { /// Vertical scrolling - Vertical { - /// The vertical [`Scrollbar`]. - scrollbar: Scrollbar, - /// The amount of spacing between the [`Scrollbar`] and the contents, if embedded. - spacing: Option<f32>, - }, + Vertical(Scrollbar), /// Horizontal scrolling - Horizontal { - /// The horizontal [`Scrollbar`]. - scrollbar: Scrollbar, - /// The amount of spacing between the [`Scrollbar`] and the contents, if embedded. - spacing: Option<f32>, - }, + Horizontal(Scrollbar), /// Both vertical and horizontal scrolling Both { /// The properties of the vertical scrollbar. @@ -228,28 +212,25 @@ impl Direction { /// Returns the horizontal [`Scrollbar`], if any. pub fn horizontal(&self) -> Option<&Scrollbar> { match self { - Self::Horizontal { scrollbar, .. } => Some(scrollbar), + Self::Horizontal(scrollbar) => Some(scrollbar), Self::Both { horizontal, .. } => Some(horizontal), - Self::Vertical { .. } => None, + Self::Vertical(_) => None, } } /// Returns the vertical [`Scrollbar`], if any. pub fn vertical(&self) -> Option<&Scrollbar> { match self { - Self::Vertical { scrollbar, .. } => Some(scrollbar), + Self::Vertical(scrollbar) => Some(scrollbar), Self::Both { vertical, .. } => Some(vertical), - Self::Horizontal { .. } => None, + Self::Horizontal(_) => None, } } } impl Default for Direction { fn default() -> Self { - Self::Vertical { - scrollbar: Scrollbar::default(), - spacing: None, - } + Self::Vertical(Scrollbar::default()) } } @@ -363,14 +344,18 @@ where limits: &layout::Limits, ) -> layout::Node { let (right_padding, bottom_padding) = match self.direction { - Direction::Vertical { - scrollbar, + Direction::Vertical(Scrollbar { + width, + margin, spacing: Some(spacing), - } => (scrollbar.width + scrollbar.margin * 2.0 + spacing, 0.0), - Direction::Horizontal { - scrollbar, + .. + }) => (width + margin * 2.0 + spacing, 0.0), + Direction::Horizontal(Scrollbar { + width, + margin, spacing: Some(spacing), - } => (0.0, scrollbar.width + scrollbar.margin * 2.0 + spacing), + .. + }) => (0.0, width + margin * 2.0 + spacing), _ => (0.0, 0.0), }; From 616689ca54942a13aac3615e571ae995ad4571b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n?= <hector@hecrj.dev> Date: Wed, 17 Jul 2024 13:00:00 +0200 Subject: [PATCH 117/657] Update `cosmic-text` and `resvg` (#2416) * Update `cosmic-text`, `glyphon`, and `resvg` * Fix slow font fallback with `Shaping::Basic` in `cosmic-text` * Update `cosmic-text` and `resvg` * Update `cosmic-text` * Fix `SelectAll` action in `editor` * Fix some panics in `graphics::text::editor` * Remove empty `if` statement in `tiny_skia::vector` * Update `cosmic-text`, `glyphon`, and `rustc-hash` --- Cargo.toml | 8 +- graphics/src/geometry/text.rs | 5 +- graphics/src/text/cache.rs | 4 +- graphics/src/text/editor.rs | 289 ++++++++++++++------------------- graphics/src/text/paragraph.rs | 8 +- tiny_skia/src/engine.rs | 10 +- tiny_skia/src/text.rs | 8 +- tiny_skia/src/vector.rs | 34 ++-- wgpu/src/image/vector.rs | 35 ++-- wgpu/src/text.rs | 8 +- 10 files changed, 187 insertions(+), 222 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b85900cf..bc566bf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,11 +138,11 @@ async-std = "1.0" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" -cosmic-text = "0.10" +cosmic-text = "0.12" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -157,8 +157,8 @@ ouroboros = "0.18" palette = "0.7" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" -resvg = "0.36" -rustc-hash = "1.0" +resvg = "0.42" +rustc-hash = "2.0" smol = "1.0" smol_str = "0.2" softbuffer = "0.4" diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index d314e85e..90147f87 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -43,6 +43,7 @@ impl Text { let mut buffer = cosmic_text::BufferLine::new( &self.content, + cosmic_text::LineEnding::default(), cosmic_text::AttrsList::new(text::to_attributes(self.font)), text::to_shaping(self.shaping), ); @@ -50,8 +51,10 @@ impl Text { let layout = buffer.layout( font_system.raw(), self.size.0, - f32::MAX, + None, cosmic_text::Wrap::None, + None, + 4, ); let translation_x = match self.horizontal_alignment { diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 822b61c4..e64d93f1 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -48,8 +48,8 @@ impl Cache { buffer.set_size( font_system, - key.bounds.width, - key.bounds.height.max(key.line_height), + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), ); buffer.set_text( font_system, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 36b4ca6e..3e6ef70c 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -17,7 +17,7 @@ use std::sync::{self, Arc}; pub struct Editor(Option<Arc<Internal>>); struct Internal { - editor: cosmic_text::Editor, + editor: cosmic_text::Editor<'static>, font: Font, bounds: Size, topmost_line_changed: Option<usize>, @@ -32,7 +32,7 @@ impl Editor { /// Returns the buffer of the [`Editor`]. pub fn buffer(&self) -> &cosmic_text::Buffer { - self.internal().editor.buffer() + buffer_from_editor(&self.internal().editor) } /// Creates a [`Weak`] reference to the [`Editor`]. @@ -101,16 +101,10 @@ impl editor::Editor for Editor { let internal = self.internal(); let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); - - match internal.editor.select_opt() { - Some(selection) => { - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; + let buffer = buffer_from_editor(&internal.editor); + match internal.editor.selection_bounds() { + Some((start, end)) => { let line_height = buffer.metrics().line_height; let selected_lines = end.line - start.line + 1; @@ -142,7 +136,8 @@ impl editor::Editor for Editor { width, y: (visual_line as i32 + visual_lines_offset) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, height: line_height, }) } else { @@ -224,7 +219,8 @@ impl editor::Editor for Editor { Cursor::Caret(Point::new( offset, (visual_lines_offset + visual_line as i32) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, )) } } @@ -252,16 +248,8 @@ impl editor::Editor for Editor { match action { // Motion events Action::Move(motion) => { - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - let (left, right) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); match motion { // These motions are performed as-is even when a selection @@ -272,17 +260,20 @@ impl editor::Editor for Editor { | Motion::DocumentEnd => { editor.action( font_system.raw(), - motion_to_action(motion), + cosmic_text::Action::Motion(to_motion(motion)), ); } // Other motions simply move the cursor to one end of the selection _ => editor.set_cursor(match motion.direction() { - Direction::Left => left, - Direction::Right => right, + Direction::Left => start, + Direction::Right => end, }), } } else { - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); } } @@ -290,103 +281,36 @@ impl editor::Editor for Editor { Action::Select(motion) => { let cursor = editor.cursor(); - if editor.select_opt().is_none() { - editor.set_select_opt(Some(cursor)); + if editor.selection_bounds().is_none() { + editor + .set_selection(cosmic_text::Selection::Normal(cursor)); } - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } Action::SelectWord => { - use unicode_segmentation::UnicodeSegmentation; - let cursor = editor.cursor(); - if let Some(line) = editor.buffer().lines.get(cursor.line) { - let (start, end) = - UnicodeSegmentation::unicode_word_indices(line.text()) - // Split words with dots - .flat_map(|(i, word)| { - word.split('.').scan(i, |current, word| { - let start = *current; - *current += word.len() + 1; - - Some((start, word)) - }) - }) - // Turn words into ranges - .map(|(i, word)| (i, i + word.len())) - // Find the word at cursor - .find(|&(start, end)| { - start <= cursor.index && cursor.index < end - }) - // Cursor is not in a word. Let's select its punctuation cluster. - .unwrap_or_else(|| { - let start = line.text()[..cursor.index] - .char_indices() - .rev() - .take_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i) - .last() - .unwrap_or(cursor.index); - - let end = line.text()[cursor.index..] - .char_indices() - .skip_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i + cursor.index) - .next() - .unwrap_or(cursor.index); - - (start, end) - }); - - if start != end { - editor.set_cursor(cosmic_text::Cursor { - index: start, - ..cursor - }); - - editor.set_select_opt(Some(cosmic_text::Cursor { - index: end, - ..cursor - })); - } - } + editor.set_selection(cosmic_text::Selection::Word(cursor)); } Action::SelectLine => { let cursor = editor.cursor(); - if let Some(line_length) = editor - .buffer() - .lines - .get(cursor.line) - .map(|line| line.text().len()) - { - editor - .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); - - editor.set_select_opt(Some(cosmic_text::Cursor { - index: line_length, - ..cursor - })); - } + editor.set_selection(cosmic_text::Selection::Line(cursor)); } Action::SelectAll => { - let buffer = editor.buffer(); + let buffer = buffer_from_editor(editor); + if buffer.lines.len() > 1 || buffer .lines @@ -394,15 +318,20 @@ impl editor::Editor for Editor { .is_some_and(|line| !line.text().is_empty()) { let cursor = editor.cursor(); - editor.set_select_opt(Some(cosmic_text::Cursor { - line: 0, - index: 0, - ..cursor - })); + + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + }, + )); editor.action( font_system.raw(), - motion_to_action(Motion::DocumentEnd), + cosmic_text::Action::Motion( + cosmic_text::Motion::BufferEnd, + ), ); } } @@ -440,10 +369,12 @@ impl editor::Editor for Editor { } let cursor = editor.cursor(); - let selection = editor.select_opt().unwrap_or(cursor); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); - internal.topmost_line_changed = - Some(cursor.min(selection).line); + internal.topmost_line_changed = Some(selection_start.line); } // Mouse events @@ -466,13 +397,9 @@ impl editor::Editor for Editor { ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } @@ -494,7 +421,7 @@ impl editor::Editor for Editor { fn min_bounds(&self) -> Size { let internal = self.internal(); - text::measure(internal.editor.buffer()) + text::measure(buffer_from_editor(&internal.editor)) } fn update( @@ -517,7 +444,10 @@ impl editor::Editor for Editor { if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer_mut_from_editor(&mut internal.editor) + .lines + .iter_mut() + { line.reset(); } @@ -528,7 +458,10 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer_mut_from_editor(&mut internal.editor) + .lines + .iter_mut() + { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); @@ -538,7 +471,7 @@ impl editor::Editor for Editor { internal.topmost_line_changed = Some(0); } - let metrics = internal.editor.buffer().metrics(); + let metrics = buffer_from_editor(&internal.editor).metrics(); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -546,7 +479,7 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - internal.editor.buffer_mut().set_metrics( + buffer_mut_from_editor(&mut internal.editor).set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); @@ -555,10 +488,10 @@ impl editor::Editor for Editor { if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - internal.editor.buffer_mut().set_size( + buffer_mut_from_editor(&mut internal.editor).set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; @@ -573,7 +506,7 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -585,12 +518,13 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, ) { let internal = self.internal(); - let buffer = internal.editor.buffer(); + let buffer = buffer_from_editor(&internal.editor); - let mut window = buffer.scroll() + buffer.visible_lines(); + let scroll = buffer.scroll(); + let mut window = (internal.bounds.height / buffer.metrics().line_height) + .ceil() as i32; - let last_visible_line = buffer - .lines + let last_visible_line = buffer.lines[scroll.line..] .iter() .enumerate() .find_map(|(i, line)| { @@ -604,7 +538,7 @@ impl editor::Editor for Editor { window -= visible_lines; None } else { - Some(i) + Some(scroll.line + i) } }) .unwrap_or(buffer.lines.len().saturating_sub(1)); @@ -626,7 +560,7 @@ impl editor::Editor for Editor { let attributes = text::to_attributes(font); - for line in &mut internal.editor.buffer_mut().lines + for line in &mut buffer_mut_from_editor(&mut internal.editor).lines [current_line..=last_visible_line] { let mut list = cosmic_text::AttrsList::new(attributes); @@ -652,7 +586,7 @@ impl editor::Editor for Editor { let _ = line.set_attrs_list(list); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -668,7 +602,8 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.editor.buffer().metrics() == other.editor.buffer().metrics() + && buffer_from_editor(&self.editor).metrics() + == buffer_from_editor(&other.editor).metrics() } } @@ -730,7 +665,8 @@ fn highlight_line( let layout = line .layout_opt() .as_ref() - .expect("Line layout should be cached"); + .map(Vec::as_slice) + .unwrap_or_default(); layout.iter().map(move |visual_line| { let start = visual_line @@ -773,34 +709,61 @@ fn highlight_line( } fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { - let visual_lines_before_start: usize = buffer - .lines + let scroll = buffer.scroll(); + + let start = scroll.line.min(line); + let end = scroll.line.max(line); + + let visual_lines_offset: usize = buffer.lines[start..] .iter() - .take(line) + .take(end - start) .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() + line.layout_opt().as_ref().map(Vec::len).unwrap_or_default() }) .sum(); - visual_lines_before_start as i32 - buffer.scroll() + visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } } -fn motion_to_action(motion: Motion) -> cosmic_text::Action { +fn to_motion(motion: Motion) -> cosmic_text::Motion { match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => cosmic_text::Action::BufferStart, - Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + } +} + +fn buffer_from_editor<'a, 'b>( + editor: &'a impl cosmic_text::Edit<'b>, +) -> &'a cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } +} + +fn buffer_mut_from_editor<'a, 'b>( + editor: &'a mut impl cosmic_text::Edit<'b>, +) -> &'a mut cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref_mut() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(_buffer) => unreachable!(), } } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 31a323ac..a5fefe8f 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -77,8 +77,8 @@ impl core::text::Paragraph for Paragraph { buffer.set_size( font_system.raw(), - text.bounds.width, - text.bounds.height, + Some(text.bounds.width), + Some(text.bounds.height), ); buffer.set_text( @@ -116,8 +116,8 @@ impl core::text::Paragraph for Paragraph { internal.buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 028b304f..898657c8 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -439,9 +439,13 @@ impl Engine { let transformation = transformation * *local_transformation; let (width, height) = buffer.size(); - let physical_bounds = - Rectangle::new(raw.position, Size::new(width, height)) - * transformation; + let physical_bounds = Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(clip_bounds.width), + height.unwrap_or(clip_bounds.height), + ), + ) * transformation; if !clip_bounds.intersects(&physical_bounds) { return; diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index c71deb10..0fc3d1f7 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -169,7 +169,13 @@ impl Pipeline { font_system.raw(), &mut self.glyph_cache, buffer, - Rectangle::new(position, Size::new(width, height)), + Rectangle::new( + position, + Size::new( + width.unwrap_or(pixels.width() as f32), + height.unwrap_or(pixels.height() as f32), + ), + ), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index bbe08cb8..8a15f47f 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -1,8 +1,7 @@ use crate::core::svg::{Data, Handle}; use crate::core::{Color, Rectangle, Size}; -use crate::graphics::text; -use resvg::usvg::{self, TreeTextToPath}; +use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use tiny_skia::Transform; @@ -80,35 +79,28 @@ struct RasterKey { impl Cache { fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { - use usvg::TreeParsing; - let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let mut svg = match handle.data() { + let svg = match handle.data() { Data::Path(path) => { fs::read_to_string(path).ok().and_then(|contents| { usvg::Tree::from_str( &contents, - &usvg::Options::default(), + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb ) .ok() }) } Data::Bytes(bytes) => { - usvg::Tree::from_data(bytes, &usvg::Options::default()).ok() + usvg::Tree::from_data( + bytes, + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb + ) + .ok() } }; - if let Some(svg) = &mut svg { - if svg.has_text_nodes() { - let mut font_system = - text::font_system().write().expect("Write font system"); - - svg.convert_text(font_system.raw().db_mut()); - } - } - let _ = entry.insert(svg); } @@ -118,11 +110,9 @@ impl Cache { fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> { let tree = self.load(handle)?; + let size = tree.size(); - Some(Size::new( - tree.size.width() as u32, - tree.size.height() as u32, - )) + Some(Size::new(size.width() as u32, size.height() as u32)) } fn draw( @@ -147,7 +137,7 @@ impl Cache { let mut image = tiny_skia::Pixmap::new(size.width, size.height)?; - let tree_size = tree.size.to_int_size(); + let tree_size = tree.size().to_int_size(); let target_size = if size.width > size.height { tree_size.scale_to_width(size.width) @@ -167,7 +157,7 @@ impl Cache { tiny_skia::Transform::default() }; - resvg::Tree::from_usvg(tree).render(transform, &mut image.as_mut()); + resvg::render(tree, transform, &mut image.as_mut()); if let Some([r, g, b, _]) = key.color { // Apply color filter diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index c6d829af..74e9924d 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -1,10 +1,9 @@ use crate::core::svg; use crate::core::{Color, Size}; -use crate::graphics::text; use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; -use resvg::usvg::{self, TreeTextToPath}; +use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use std::fs; @@ -21,7 +20,7 @@ impl Svg { pub fn viewport_dimensions(&self) -> Size<u32> { match self { Svg::Loaded(tree) => { - let size = tree.size; + let size = tree.size(); Size::new(size.width() as u32, size.height() as u32) } @@ -45,38 +44,33 @@ type ColorFilter = Option<[u8; 4]>; impl Cache { /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { - use usvg::TreeParsing; - if self.svgs.contains_key(&handle.id()) { return self.svgs.get(&handle.id()).unwrap(); } - let mut svg = match handle.data() { + let svg = match handle.data() { svg::Data::Path(path) => fs::read_to_string(path) .ok() .and_then(|contents| { - usvg::Tree::from_str(&contents, &usvg::Options::default()) - .ok() + usvg::Tree::from_str( + &contents, + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb + ) + .ok() }) .map(Svg::Loaded) .unwrap_or(Svg::NotFound), svg::Data::Bytes(bytes) => { - match usvg::Tree::from_data(bytes, &usvg::Options::default()) { + match usvg::Tree::from_data( + bytes, + &usvg::Options::default(), // TODO: Set usvg::Options::fontdb + ) { Ok(tree) => Svg::Loaded(tree), Err(_) => Svg::NotFound, } } }; - if let Svg::Loaded(svg) = &mut svg { - if svg.has_text_nodes() { - let mut font_system = - text::font_system().write().expect("Write font system"); - - svg.convert_text(font_system.raw().db_mut()); - } - } - self.should_trim = true; let _ = self.svgs.insert(handle.id(), svg); @@ -127,7 +121,7 @@ impl Cache { // It would be cool to be able to smooth resize the `svg` example. let mut img = tiny_skia::Pixmap::new(width, height)?; - let tree_size = tree.size.to_int_size(); + let tree_size = tree.size().to_int_size(); let target_size = if width > height { tree_size.scale_to_width(width) @@ -147,8 +141,7 @@ impl Cache { tiny_skia::Transform::default() }; - resvg::Tree::from_usvg(tree) - .render(transform, &mut img.as_mut()); + resvg::render(tree, transform, &mut img.as_mut()); let mut rgba = img.take(); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 05db5f80..bf7eae18 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -585,7 +585,13 @@ fn prepare( ( buffer.as_ref(), - Rectangle::new(raw.position, Size::new(width, height)), + Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(layer_bounds.width), + height.unwrap_or(layer_bounds.height), + ), + ), alignment::Horizontal::Left, alignment::Vertical::Top, raw.color, From ffb520fb3703ce4ece9fb6d5ee2c7aa0b846879f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 17 Jul 2024 18:47:58 +0200 Subject: [PATCH 118/657] Decouple caching from `Paragraph` API --- core/src/renderer/null.rs | 2 +- core/src/text.rs | 3 +- core/src/text/paragraph.rs | 90 ++++++++++++++++++++++++++++------ core/src/widget/text.rs | 11 +++-- graphics/src/text/paragraph.rs | 76 ++++++++-------------------- widget/src/pick_list.rs | 9 ++-- widget/src/text_input.rs | 32 ++++++------ 7 files changed, 128 insertions(+), 95 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e8709dbc..560b5b43 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -79,7 +79,7 @@ impl text::Paragraph for () { fn resize(&mut self, _new_bounds: Size) {} - fn compare(&self, _text: Text<&str>) -> text::Difference { + fn compare(&self, _text: Text<()>) -> text::Difference { text::Difference::None } diff --git a/core/src/text.rs b/core/src/text.rs index b30feae0..e437a635 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,8 +1,7 @@ //! Draw and interact with text. -mod paragraph; - pub mod editor; pub mod highlighter; +pub mod paragraph; pub use editor::Editor; pub use highlighter::Highlighter; diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 8ff04015..66cadb5c 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -1,3 +1,4 @@ +//! Draw paragraphs. use crate::alignment; use crate::text::{Difference, Hit, Text}; use crate::{Point, Size}; @@ -15,7 +16,7 @@ pub trait Paragraph: Sized + Default { /// Compares the [`Paragraph`] with some desired [`Text`] and returns the /// [`Difference`]. - fn compare(&self, text: Text<&str, Self::Font>) -> Difference; + fn compare(&self, text: Text<(), Self::Font>) -> Difference; /// Returns the horizontal alignment of the [`Paragraph`]. fn horizontal_alignment(&self) -> alignment::Horizontal; @@ -34,19 +35,6 @@ pub trait Paragraph: Sized + Default { /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; - /// Updates the [`Paragraph`] to match the given [`Text`], if needed. - fn update(&mut self, text: Text<&str, Self::Font>) { - match self.compare(text) { - Difference::None => {} - Difference::Bounds => { - self.resize(text.bounds); - } - Difference::Shape => { - *self = Self::with_text(text); - } - } - } - /// Returns the minimum width that can fit the contents of the [`Paragraph`]. fn min_width(&self) -> f32 { self.min_bounds().width @@ -57,3 +45,77 @@ pub trait Paragraph: Sized + Default { self.min_bounds().height } } + +/// A [`Paragraph`] of plain text. +#[derive(Debug, Clone, Default)] +pub struct Plain<P: Paragraph> { + raw: P, + content: String, +} + +impl<P: Paragraph> Plain<P> { + /// Creates a new [`Plain`] paragraph. + pub fn new(text: Text<&str, P::Font>) -> Self { + let content = text.content.to_owned(); + + Self { + raw: P::with_text(text), + content, + } + } + + /// Updates the plain [`Paragraph`] to match the given [`Text`], if needed. + pub fn update(&mut self, text: Text<&str, P::Font>) { + if self.content != text.content { + text.content.clone_into(&mut self.content); + self.raw = P::with_text(text); + return; + } + + match self.raw.compare(Text { + content: (), + bounds: text.bounds, + size: text.size, + line_height: text.line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + }) { + Difference::None => {} + Difference::Bounds => { + self.raw.resize(text.bounds); + } + Difference::Shape => { + self.raw = P::with_text(text); + } + } + } + + /// Returns the horizontal alignment of the [`Paragraph`]. + pub fn horizontal_alignment(&self) -> alignment::Horizontal { + self.raw.horizontal_alignment() + } + + /// Returns the vertical alignment of the [`Paragraph`]. + pub fn vertical_alignment(&self) -> alignment::Vertical { + self.raw.vertical_alignment() + } + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Paragraph`]. + pub fn min_bounds(&self) -> Size { + self.raw.min_bounds() + } + + /// Returns the minimum width that can fit the contents of the + /// [`Paragraph`]. + pub fn min_width(&self) -> f32 { + self.raw.min_width() + } + + /// Returns the cached [`Paragraph`]. + pub fn raw(&self) -> &P { + &self.raw + } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 91c9893d..2aeb0765 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -3,7 +3,8 @@ use crate::alignment; use crate::layout; use crate::mouse; use crate::renderer; -use crate::text::{self, Paragraph}; +use crate::text; +use crate::text::paragraph::{self, Paragraph}; use crate::widget::tree::{self, Tree}; use crate::{ Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, @@ -155,7 +156,7 @@ where /// The internal state of a [`Text`] widget. #[derive(Debug, Default)] -pub struct State<P: Paragraph>(P); +pub struct State<P: Paragraph>(paragraph::Plain<P>); impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Text<'a, Theme, Renderer> @@ -168,7 +169,9 @@ where } fn state(&self) -> tree::State { - tree::State::new(State(Renderer::Paragraph::default())) + tree::State::new(State::<Renderer::Paragraph>( + paragraph::Plain::default(), + )) } fn size(&self) -> Size<Length> { @@ -294,7 +297,7 @@ pub fn draw<Renderer>( }; renderer.fill_paragraph( - paragraph, + paragraph.raw(), Point::new(x, y), appearance.color.unwrap_or(style.text_color), *viewport, diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index a5fefe8f..ea59c0af 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,8 +1,8 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, LineHeight, Shaping, Text}; -use crate::core::{Font, Pixels, Point, Size}; +use crate::core::text::{Hit, Shaping, Text}; +use crate::core::{Font, Point, Size}; use crate::text; use std::fmt; @@ -10,11 +10,11 @@ use std::sync::{self, Arc}; /// A bunch of text. #[derive(Clone, PartialEq)] -pub struct Paragraph(Option<Arc<Internal>>); +pub struct Paragraph(Arc<Internal>); +#[derive(Clone)] struct Internal { buffer: cosmic_text::Buffer, - content: String, // TODO: Reuse from `buffer` (?) font: Font, shaping: Shaping, horizontal_alignment: alignment::Horizontal, @@ -52,9 +52,7 @@ impl Paragraph { } fn internal(&self) -> &Arc<Internal> { - self.0 - .as_ref() - .expect("paragraph should always be initialized") + &self.0 } } @@ -90,9 +88,8 @@ impl core::text::Paragraph for Paragraph { let min_bounds = text::measure(&buffer); - Self(Some(Arc::new(Internal { + Self(Arc::new(Internal { buffer, - content: text.content.to_owned(), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, @@ -100,59 +97,31 @@ impl core::text::Paragraph for Paragraph { bounds: text.bounds, min_bounds, version: font_system.version(), - }))) + })) } fn resize(&mut self, new_bounds: Size) { - let paragraph = self - .0 - .take() - .expect("paragraph should always be initialized"); + let paragraph = Arc::make_mut(&mut self.0); - match Arc::try_unwrap(paragraph) { - Ok(mut internal) => { - let mut font_system = - text::font_system().write().expect("Write font system"); + let mut font_system = + text::font_system().write().expect("Write font system"); - internal.buffer.set_size( - font_system.raw(), - Some(new_bounds.width), - Some(new_bounds.height), - ); + paragraph.buffer.set_size( + font_system.raw(), + Some(new_bounds.width), + Some(new_bounds.height), + ); - internal.bounds = new_bounds; - internal.min_bounds = text::measure(&internal.buffer); - - self.0 = Some(Arc::new(internal)); - } - Err(internal) => { - let metrics = internal.buffer.metrics(); - - // If there is a strong reference somewhere, we recompute the - // buffer from scratch - *self = Self::with_text(Text { - content: &internal.content, - bounds: internal.bounds, - size: Pixels(metrics.font_size), - line_height: LineHeight::Absolute(Pixels( - metrics.line_height, - )), - font: internal.font, - horizontal_alignment: internal.horizontal_alignment, - vertical_alignment: internal.vertical_alignment, - shaping: internal.shaping, - }); - } - } + paragraph.bounds = new_bounds; + paragraph.min_bounds = text::measure(¶graph.buffer); } - fn compare(&self, text: Text<&str>) -> core::text::Difference { + fn compare(&self, text: Text<()>) -> core::text::Difference { let font_system = text::font_system().read().expect("Read font system"); let paragraph = self.internal(); let metrics = paragraph.buffer.metrics(); if paragraph.version != font_system.version - || paragraph.content != text.content || metrics.font_size != text.size.0 || metrics.line_height != text.line_height.to_absolute(text.size).0 || paragraph.font != text.font @@ -231,7 +200,7 @@ impl core::text::Paragraph for Paragraph { impl Default for Paragraph { fn default() -> Self { - Self(Some(Arc::new(Internal::default()))) + Self(Arc::new(Internal::default())) } } @@ -240,7 +209,6 @@ impl fmt::Debug for Paragraph { let paragraph = self.internal(); f.debug_struct("Paragraph") - .field("content", ¶graph.content) .field("font", ¶graph.font) .field("shaping", ¶graph.shaping) .field("horizontal_alignment", ¶graph.horizontal_alignment) @@ -253,8 +221,7 @@ impl fmt::Debug for Paragraph { impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { - self.content == other.content - && self.font == other.font + self.font == other.font && self.shaping == other.shaping && self.horizontal_alignment == other.horizontal_alignment && self.vertical_alignment == other.vertical_alignment @@ -271,7 +238,6 @@ impl Default for Internal { font_size: 1.0, line_height: 1.0, }), - content: String::new(), font: Font::default(), shaping: Shaping::default(), horizontal_alignment: alignment::Horizontal::Left, @@ -298,7 +264,7 @@ pub struct Weak { impl Weak { /// Tries to update the reference into a [`Paragraph`]. pub fn upgrade(&self) -> Option<Paragraph> { - self.raw.upgrade().map(Some).map(Paragraph) + self.raw.upgrade().map(Paragraph) } } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 97de5b48..f7f7b65b 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -6,7 +6,8 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph; +use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ @@ -622,8 +623,8 @@ struct State<P: text::Paragraph> { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, - options: Vec<P>, - placeholder: P, + options: Vec<paragraph::Plain<P>>, + placeholder: paragraph::Plain<P>, } impl<P: text::Paragraph> State<P> { @@ -635,7 +636,7 @@ impl<P: text::Paragraph> State<P> { is_open: bool::default(), hovered_option: Option::default(), options: Vec::new(), - placeholder: P::default(), + placeholder: paragraph::Plain::default(), } } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index ba2fbc13..a0fe14a0 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -19,7 +19,8 @@ use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph; +use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; @@ -360,7 +361,7 @@ where let icon_layout = children_layout.next().unwrap(); renderer.fill_paragraph( - &state.icon, + state.icon.raw(), icon_layout.bounds().center(), style.icon, *viewport, @@ -378,7 +379,7 @@ where cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, position, ); @@ -415,14 +416,14 @@ where let (left_position, left_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, left, ); let (right_position, right_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, right, ); @@ -469,9 +470,9 @@ where renderer.fill_paragraph( if text.is_empty() { - &state.placeholder + state.placeholder.raw() } else { - &state.value + state.value.raw() }, Point::new(text_bounds.x, text_bounds.center_y()) - Vector::new(offset, 0.0), @@ -1178,9 +1179,9 @@ pub fn select_all<T>(id: Id) -> Task<T> { /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State<P: text::Paragraph> { - value: P, - placeholder: P, - icon: P, + value: paragraph::Plain<P>, + placeholder: paragraph::Plain<P>, + icon: paragraph::Plain<P>, is_focused: Option<Focus>, is_dragging: bool, is_pasting: Option<Value>, @@ -1212,9 +1213,9 @@ impl<P: text::Paragraph> State<P> { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { - value: P::default(), - placeholder: P::default(), - icon: P::default(), + value: paragraph::Plain::default(), + placeholder: paragraph::Plain::default(), + icon: paragraph::Plain::default(), is_focused: None, is_dragging: false, is_pasting: None, @@ -1319,7 +1320,7 @@ fn offset<P: text::Paragraph>( }; let (_, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, focus_position, ); @@ -1357,6 +1358,7 @@ fn find_cursor_position<P: text::Paragraph>( let char_offset = state .value + .raw() .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; @@ -1386,7 +1388,7 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = Renderer::Paragraph::with_text(Text { + state.value = paragraph::Plain::new(Text { font, line_height, content: &value.to_string(), From 910eb72a0620b34e5b3d7793bbd5ab7290e08dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 17 Jul 2024 22:04:11 +0200 Subject: [PATCH 119/657] Implement `rich_text` widget and `markdown` example --- core/src/renderer/null.rs | 5 + core/src/text.rs | 148 +++++++++++++++ core/src/text/paragraph.rs | 5 +- core/src/widget/text.rs | 91 +--------- examples/markdown/Cargo.toml | 12 ++ examples/markdown/src/main.rs | 172 ++++++++++++++++++ graphics/src/text.rs | 13 +- graphics/src/text/paragraph.rs | 80 ++++++++- widget/src/helpers.rs | 39 +++- widget/src/text.rs | 4 + widget/src/text/rich.rs | 317 +++++++++++++++++++++++++++++++++ 11 files changed, 787 insertions(+), 99 deletions(-) create mode 100644 examples/markdown/Cargo.toml create mode 100644 examples/markdown/src/main.rs create mode 100644 widget/src/text/rich.rs diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 560b5b43..f9d1a5b0 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -77,6 +77,11 @@ impl text::Paragraph for () { fn with_text(_text: Text<&str>) -> Self {} + fn with_spans( + _text: Text<&[text::Span<'_, Self::Font>], Self::Font>, + ) -> Self { + } + fn resize(&mut self, _new_bounds: Size) {} fn compare(&self, _text: Text<()>) -> text::Difference { diff --git a/core/src/text.rs b/core/src/text.rs index e437a635..d73eb94a 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -10,6 +10,7 @@ pub use paragraph::Paragraph; use crate::alignment; use crate::{Color, Pixels, Point, Rectangle, Size}; +use std::borrow::Cow; use std::hash::{Hash, Hasher}; /// A paragraph. @@ -220,3 +221,150 @@ pub trait Renderer: crate::Renderer { clip_bounds: Rectangle, ); } + +/// A span of text. +#[derive(Debug, Clone, PartialEq)] +pub struct Span<'a, Font = crate::Font> { + /// The [`Fragment`] of text. + pub text: Fragment<'a>, + /// The size of the [`Span`] in [`Pixels`]. + pub size: Option<Pixels>, + /// The [`LineHeight`] of the [`Span`]. + pub line_height: Option<LineHeight>, + /// The font of the [`Span`]. + pub font: Option<Font>, + /// The [`Color`] of the [`Span`]. + pub color: Option<Color>, +} + +impl<'a, Font> Span<'a, Font> { + /// Creates a new [`Span`] of text with the given text fragment. + pub fn new(fragment: impl IntoFragment<'a>) -> Self { + Self { + text: fragment.into_fragment(), + size: None, + line_height: None, + font: None, + color: None, + } + } + + /// Sets the size of the [`Span`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the [`LineHeight`] of the [`Span`]. + pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { + self.line_height = Some(line_height.into()); + self + } + + /// Sets the font of the [`Span`]. + pub fn font(mut self, font: impl Into<Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the [`Color`] of the [`Span`]. + pub fn color(mut self, color: impl Into<Color>) -> Self { + self.color = Some(color.into()); + self + } + + /// Turns the [`Span`] into a static one. + pub fn to_static(self) -> Span<'static, Font> { + Span { + text: Cow::Owned(self.text.into_owned()), + size: self.size, + line_height: self.line_height, + font: self.font, + color: self.color, + } + } +} + +impl<'a, Font> From<&'a str> for Span<'a, Font> { + fn from(value: &'a str) -> Self { + Span::new(value) + } +} + +/// A fragment of [`Text`]. +/// +/// This is just an alias to a string that may be either +/// borrowed or owned. +pub type Fragment<'a> = Cow<'a, str>; + +/// A trait for converting a value to some text [`Fragment`]. +pub trait IntoFragment<'a> { + /// Converts the value to some text [`Fragment`]. + fn into_fragment(self) -> Fragment<'a>; +} + +impl<'a> IntoFragment<'a> for Fragment<'a> { + fn into_fragment(self) -> Fragment<'a> { + self + } +} + +impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self) + } +} + +impl<'a> IntoFragment<'a> for &'a str { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self) + } +} + +impl<'a> IntoFragment<'a> for &'a String { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self.as_str()) + } +} + +impl<'a> IntoFragment<'a> for String { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self) + } +} + +macro_rules! into_fragment { + ($type:ty) => { + impl<'a> IntoFragment<'a> for $type { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self.to_string()) + } + } + + impl<'a> IntoFragment<'a> for &$type { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self.to_string()) + } + } + }; +} + +into_fragment!(char); +into_fragment!(bool); + +into_fragment!(u8); +into_fragment!(u16); +into_fragment!(u32); +into_fragment!(u64); +into_fragment!(u128); +into_fragment!(usize); + +into_fragment!(i8); +into_fragment!(i16); +into_fragment!(i32); +into_fragment!(i64); +into_fragment!(i128); +into_fragment!(isize); + +into_fragment!(f32); +into_fragment!(f64); diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 66cadb5c..4ee83798 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -1,6 +1,6 @@ //! Draw paragraphs. use crate::alignment; -use crate::text::{Difference, Hit, Text}; +use crate::text::{Difference, Hit, Span, Text}; use crate::{Point, Size}; /// A text paragraph. @@ -11,6 +11,9 @@ pub trait Paragraph: Sized + Default { /// Creates a new [`Paragraph`] laid out with the given [`Text`]. fn with_text(text: Text<&str, Self::Font>) -> Self; + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_spans(text: Text<&[Span<'_, Self::Font>], Self::Font>) -> Self; + /// Lays out the [`Paragraph`] with some new boundaries. fn resize(&mut self, new_bounds: Size); diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 2aeb0765..d0ecd27b 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -11,8 +11,6 @@ use crate::{ Widget, }; -use std::borrow::Cow; - pub use text::{LineHeight, Shaping}; /// A paragraph of text. @@ -22,7 +20,7 @@ where Theme: Catalog, Renderer: text::Renderer, { - fragment: Fragment<'a>, + fragment: text::Fragment<'a>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -40,7 +38,7 @@ where Renderer: text::Renderer, { /// Create a new fragment of [`Text`] with the given contents. - pub fn new(fragment: impl IntoFragment<'a>) -> Self { + pub fn new(fragment: impl text::IntoFragment<'a>) -> Self { Text { fragment: fragment.into_fragment(), size: None, @@ -216,7 +214,7 @@ where let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); let style = theme.style(&self.class); - draw(renderer, defaults, layout, state, style, viewport); + draw(renderer, defaults, layout, state.0.raw(), style, viewport); } } @@ -275,13 +273,12 @@ pub fn draw<Renderer>( renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, - state: &State<Renderer::Paragraph>, + paragraph: &Renderer::Paragraph, appearance: Style, viewport: &Rectangle, ) where Renderer: text::Renderer, { - let State(ref paragraph) = state; let bounds = layout.bounds(); let x = match paragraph.horizontal_alignment() { @@ -297,7 +294,7 @@ pub fn draw<Renderer>( }; renderer.fill_paragraph( - paragraph.raw(), + paragraph, Point::new(x, y), appearance.color.unwrap_or(style.text_color), *viewport, @@ -415,81 +412,3 @@ pub fn danger(theme: &Theme) -> Style { color: Some(theme.palette().danger), } } - -/// A fragment of [`Text`]. -/// -/// This is just an alias to a string that may be either -/// borrowed or owned. -pub type Fragment<'a> = Cow<'a, str>; - -/// A trait for converting a value to some text [`Fragment`]. -pub trait IntoFragment<'a> { - /// Converts the value to some text [`Fragment`]. - fn into_fragment(self) -> Fragment<'a>; -} - -impl<'a> IntoFragment<'a> for Fragment<'a> { - fn into_fragment(self) -> Fragment<'a> { - self - } -} - -impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Borrowed(self) - } -} - -impl<'a> IntoFragment<'a> for &'a str { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Borrowed(self) - } -} - -impl<'a> IntoFragment<'a> for &'a String { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Borrowed(self.as_str()) - } -} - -impl<'a> IntoFragment<'a> for String { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Owned(self) - } -} - -macro_rules! into_fragment { - ($type:ty) => { - impl<'a> IntoFragment<'a> for $type { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Owned(self.to_string()) - } - } - - impl<'a> IntoFragment<'a> for &$type { - fn into_fragment(self) -> Fragment<'a> { - Fragment::Owned(self.to_string()) - } - } - }; -} - -into_fragment!(char); -into_fragment!(bool); - -into_fragment!(u8); -into_fragment!(u16); -into_fragment!(u32); -into_fragment!(u64); -into_fragment!(u128); -into_fragment!(usize); - -into_fragment!(i8); -into_fragment!(i16); -into_fragment!(i32); -into_fragment!(i64); -into_fragment!(i128); -into_fragment!(isize); - -into_fragment!(f32); -into_fragment!(f64); diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml new file mode 100644 index 00000000..f9bf4042 --- /dev/null +++ b/examples/markdown/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "markdown" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug"] + +pulldown-cmark = "0.11" diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs new file mode 100644 index 00000000..43adaf72 --- /dev/null +++ b/examples/markdown/src/main.rs @@ -0,0 +1,172 @@ +use iced::font; +use iced::padding; +use iced::widget::{ + self, column, container, rich_text, row, span, text_editor, +}; +use iced::{Element, Fill, Font, Task, Theme}; + +pub fn main() -> iced::Result { + iced::application("Markdown - Iced", Markdown::update, Markdown::view) + .theme(Markdown::theme) + .run_with(Markdown::new) +} + +struct Markdown { + content: text_editor::Content, +} + +#[derive(Debug, Clone)] +enum Message { + Edit(text_editor::Action), +} + +impl Markdown { + fn new() -> (Self, Task<Message>) { + ( + Self { + content: text_editor::Content::with_text( + "# Markdown Editor\nType your Markdown here...", + ), + }, + widget::focus_next(), + ) + } + fn update(&mut self, message: Message) { + match message { + Message::Edit(action) => { + self.content.perform(action); + } + } + } + + fn view(&self) -> Element<Message> { + let editor = text_editor(&self.content) + .on_action(Message::Edit) + .height(Fill) + .padding(10) + .font(Font::MONOSPACE); + + let preview = { + let markdown = self.content.text(); + let parser = pulldown_cmark::Parser::new(&markdown); + + let mut strong = false; + let mut emphasis = false; + let mut heading = None; + let mut spans = Vec::new(); + + let items = parser.filter_map(|event| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Strong => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis => { + emphasis = true; + None + } + pulldown_cmark::Tag::Heading { level, .. } => { + heading = Some(level); + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Emphasis => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strong => { + strong = false; + None + } + pulldown_cmark::TagEnd::Heading(_) => { + heading = None; + Some( + container(rich_text(spans.drain(..))) + .padding(padding::bottom(5)) + .into(), + ) + } + pulldown_cmark::TagEnd::Paragraph => Some( + container(rich_text(spans.drain(..))) + .padding(padding::bottom(15)) + .into(), + ), + pulldown_cmark::TagEnd::CodeBlock => Some( + container( + container( + rich_text(spans.drain(..)) + .font(Font::MONOSPACE), + ) + .width(Fill) + .padding(10) + .style(container::rounded_box), + ) + .padding(padding::bottom(15)) + .into(), + ), + _ => None, + }, + pulldown_cmark::Event::Text(text) => { + let span = span(text.into_string()); + + let span = match heading { + None => span, + Some(heading) => span.size(match heading { + pulldown_cmark::HeadingLevel::H1 => 32, + pulldown_cmark::HeadingLevel::H2 => 28, + pulldown_cmark::HeadingLevel::H3 => 24, + pulldown_cmark::HeadingLevel::H4 => 20, + pulldown_cmark::HeadingLevel::H5 => 16, + pulldown_cmark::HeadingLevel::H6 => 16, + }), + }; + + let span = if strong || emphasis { + span.font(Font { + weight: if strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + spans.push(span); + + None + } + pulldown_cmark::Event::Code(code) => { + spans.push(span(code.into_string()).font(Font::MONOSPACE)); + None + } + pulldown_cmark::Event::SoftBreak => { + spans.push(span(" ")); + None + } + pulldown_cmark::Event::HardBreak => { + spans.push(span("\n")); + None + } + _ => None, + }); + + column(items).width(Fill) + }; + + row![editor, preview].spacing(10).padding(10).into() + } + + fn theme(&self) -> Theme { + Theme::TokyoNight + } +} diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 30269e69..23ec14d4 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -232,13 +232,14 @@ impl PartialEq for Raw { /// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { - let (width, total_lines) = buffer - .layout_runs() - .fold((0.0, 0usize), |(width, total_lines), run| { - (run.line_w.max(width), total_lines + 1) - }); + let (width, height) = + buffer + .layout_runs() + .fold((0.0, 0.0), |(width, height), run| { + (run.line_w.max(width), height + run.line_height) + }); - Size::new(width, total_lines as f32 * buffer.metrics().line_height) + Size::new(width, height) } /// Returns the attributes of the given [`Font`]. diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index ea59c0af..37fa97f2 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,7 +1,7 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, Shaping, Text}; +use crate::core::text::{Hit, Shaping, Span, Text}; use crate::core::{Font, Point, Size}; use crate::text; @@ -60,7 +60,7 @@ impl core::text::Paragraph for Paragraph { type Font = Font; fn with_text(text: Text<&str>) -> Self { - log::trace!("Allocating paragraph: {}", text.content); + log::trace!("Allocating plain paragraph: {}", text.content); let mut font_system = text::font_system().write().expect("Write font system"); @@ -100,6 +100,82 @@ impl core::text::Paragraph for Paragraph { })) } + fn with_spans(text: Text<&[Span<'_>]>) -> Self { + log::trace!("Allocating rich paragraph: {:?}", text.content); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::Buffer::new( + font_system.raw(), + cosmic_text::Metrics::new( + text.size.into(), + text.line_height.to_absolute(text.size).into(), + ), + ); + + buffer.set_size( + font_system.raw(), + Some(text.bounds.width), + Some(text.bounds.height), + ); + + buffer.set_rich_text( + font_system.raw(), + text.content.iter().map(|span| { + let attrs = cosmic_text::Attrs::new(); + + let attrs = if let Some(font) = span.font { + attrs + .family(text::to_family(font.family)) + .weight(text::to_weight(font.weight)) + .stretch(text::to_stretch(font.stretch)) + .style(text::to_style(font.style)) + } else { + text::to_attributes(text.font) + }; + + let attrs = match (span.size, span.line_height) { + (None, None) => attrs, + _ => { + let size = span.size.unwrap_or(text.size); + + attrs.metrics(cosmic_text::Metrics::new( + size.into(), + span.line_height + .unwrap_or(text.line_height) + .to_absolute(size) + .into(), + )) + } + }; + + let attrs = if let Some(color) = span.color { + attrs.color(text::to_color(color)) + } else { + attrs + }; + + (span.text.as_ref(), attrs) + }), + text::to_attributes(text.font), + text::to_shaping(text.shaping), + ); + + let min_bounds = text::measure(&buffer); + + Self(Arc::new(Internal { + buffer, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + bounds: text.bounds, + min_bounds, + version: font_system.version(), + })) + } + fn resize(&mut self, new_bounds: Size) { let paragraph = Arc::make_mut(&mut self.0); diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1f282f54..66b37ccb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -112,6 +112,19 @@ macro_rules! text { }; } +/// Creates some [`Rich`] text with the given spans. +/// +/// [`Rich`]: text::Rich +#[macro_export] +macro_rules! rich_text { + () => ( + $crate::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::text::Rich::with_spans([$($crate::text::Span::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -646,8 +659,6 @@ where } /// Creates a new [`Text`] widget with the provided content. -/// -/// [`Text`]: core::widget::Text pub fn text<'a, Theme, Renderer>( text: impl text::IntoFragment<'a>, ) -> Text<'a, Theme, Renderer> @@ -659,8 +670,6 @@ where } /// Creates a new [`Text`] widget that displays the provided value. -/// -/// [`Text`]: core::widget::Text pub fn value<'a, Theme, Renderer>( value: impl ToString, ) -> Text<'a, Theme, Renderer> @@ -671,6 +680,28 @@ where Text::new(value.to_string()) } +/// Creates a new [`Rich`] text widget with the provided spans. +/// +/// [`Rich`]: text::Rich +pub fn rich_text<'a, Theme, Renderer>( + spans: impl IntoIterator<Item = text::Span<'a, Renderer::Font>>, +) -> text::Rich<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: core::text::Renderer, +{ + text::Rich::with_spans(spans) +} + +/// Creates a new [`Span`] of text with the provided content. +/// +/// [`Span`]: text::Span +pub fn span<'a, Font>( + text: impl text::IntoFragment<'a>, +) -> text::Span<'a, Font> { + text::Span::new(text) +} + /// Creates a new [`Checkbox`]. /// /// [`Checkbox`]: crate::Checkbox diff --git a/widget/src/text.rs b/widget/src/text.rs index 0d689295..c32f9be1 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,5 +1,9 @@ //! Draw and interact with text. +mod rich; + +pub use crate::core::text::{Fragment, IntoFragment, Span}; pub use crate::core::widget::text::*; +pub use rich::Rich; /// A paragraph. pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> = diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs new file mode 100644 index 00000000..dc784310 --- /dev/null +++ b/widget/src/text/rich.rs @@ -0,0 +1,317 @@ +use crate::core::alignment; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::{Paragraph, Span}; +use crate::core::widget::text::{ + self, Catalog, LineHeight, Shaping, Style, StyleFn, +}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + self, Color, Element, Length, Pixels, Rectangle, Size, Widget, +}; + +/// A bunch of [`Rich`] text. +#[derive(Debug)] +pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + spans: Vec<Span<'a, Renderer::Font>>, + size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + font: Option<Renderer::Font>, + align_x: alignment::Horizontal, + align_y: alignment::Vertical, + class: Theme::Class<'a>, +} + +impl<'a, Theme, Renderer> Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + /// Creates a new empty [`Rich`] text. + pub fn new() -> Self { + Self { + spans: Vec::new(), + size: None, + line_height: LineHeight::default(), + width: Length::Shrink, + height: Length::Shrink, + font: None, + align_x: alignment::Horizontal::Left, + align_y: alignment::Vertical::Top, + class: Theme::default(), + } + } + + /// Creates a new [`Rich`] text with the given text spans. + pub fn with_spans( + spans: impl IntoIterator<Item = Span<'a, Renderer::Font>>, + ) -> Self { + Self { + spans: spans.into_iter().collect(), + ..Self::new() + } + } + + /// Sets the default size of the [`Rich`] text. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the defualt [`LineHeight`] of the [`Rich`] text. + pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { + self.line_height = line_height.into(); + self + } + + /// Sets the default font of the [`Rich`] text. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the width of the [`Rich`] text boundaries. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Rich`] text boundaries. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Centers the [`Rich`] text, both horizontally and vertically. + pub fn center(self) -> Self { + self.align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center) + } + + /// Sets the [`alignment::Horizontal`] of the [`Rich`] text. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.align_x = alignment.into(); + self + } + + /// Sets the [`alignment::Vertical`] of the [`Rich`] text. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.align_y = alignment.into(); + self + } + + /// Sets the default style of the [`Rich`] text. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the default [`Color`] of the [`Rich`] text. + pub fn color(self, color: impl Into<Color>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.color_maybe(Some(color)) + } + + /// Sets the default [`Color`] of the [`Rich`] text, if `Some`. + pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + let color = color.map(Into::into); + + self.style(move |_theme| Style { color }) + } + + /// Sets the default style class of the [`Rich`] text. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } + + /// Adds a new text [`Span`] to the [`Rich`] text. + pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { + self.spans.push(span.into()); + self + } +} + +impl<'a, Theme, Renderer> Default for Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +struct State<P: Paragraph> { + spans: Vec<Span<'static, P::Font>>, + paragraph: P, +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + spans: Vec::new(), + paragraph: Renderer::Paragraph::default(), + }) + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + tree.state.downcast_mut::<State<Renderer::Paragraph>>(), + renderer, + limits, + self.width, + self.height, + self.spans.as_slice(), + self.line_height, + self.size, + self.font, + self.align_x, + self.align_y, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + _cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let style = theme.style(&self.class); + + text::draw( + renderer, + defaults, + layout, + &state.paragraph, + style, + viewport, + ); + } +} + +fn layout<Renderer>( + state: &mut State<Renderer::Paragraph>, + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + spans: &[Span<'_, Renderer::Font>], + line_height: LineHeight, + size: Option<Pixels>, + font: Option<Renderer::Font>, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, +) -> layout::Node +where + Renderer: core::text::Renderer, +{ + layout::sized(limits, width, height, |limits| { + let bounds = limits.max(); + + let size = size.unwrap_or_else(|| renderer.default_size()); + let font = font.unwrap_or_else(|| renderer.default_font()); + + let text_with_spans = || core::Text { + content: spans, + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + }; + + if state.spans != spans { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + state.spans = spans.iter().cloned().map(Span::to_static).collect(); + } else { + match state.paragraph.compare(core::Text { + content: (), + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + }) { + core::text::Difference::None => {} + core::text::Difference::Bounds => { + state.paragraph.resize(bounds); + } + core::text::Difference::Shape => { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + } + } + } + + state.paragraph.min_bounds() + }) +} + +impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + fn from( + text: Rich<'a, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(text) + } +} From 904704d7c1b006c850654dcf3bf9e856e23cb317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 18 Jul 2024 13:14:56 +0200 Subject: [PATCH 120/657] Flesh out the `markdown` example a bit more --- core/src/text.rs | 12 ++ examples/editor/src/main.rs | 2 +- examples/markdown/Cargo.toml | 2 +- examples/markdown/overview.md | 102 ++++++++++ examples/markdown/src/main.rs | 356 ++++++++++++++++++++++------------ highlighter/src/lib.rs | 37 +++- widget/src/helpers.rs | 4 +- widget/src/text.rs | 2 +- widget/src/text/rich.rs | 30 ++- widget/src/text_editor.rs | 29 +++ 10 files changed, 435 insertions(+), 141 deletions(-) create mode 100644 examples/markdown/overview.md diff --git a/core/src/text.rs b/core/src/text.rs index d73eb94a..22cfce13 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -267,12 +267,24 @@ impl<'a, Font> Span<'a, Font> { self } + /// Sets the font of the [`Span`], if any. + pub fn font_maybe(mut self, font: Option<impl Into<Font>>) -> Self { + self.font = font.map(Into::into); + self + } + /// Sets the [`Color`] of the [`Span`]. pub fn color(mut self, color: impl Into<Color>) -> Self { self.color = Some(color.into()); self } + /// Sets the [`Color`] of the [`Span`], if any. + pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self { + self.color = color.map(Into::into); + self + } + /// Turns the [`Span`] into a static one. pub fn to_static(self) -> Span<'static, Font> { Span { diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 71b1a719..9ffb4d1a 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -189,7 +189,7 @@ impl Editor { .highlight::<Highlighter>( highlighter::Settings { theme: self.theme, - extension: self + token: self .file .as_deref() .and_then(Path::extension) diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml index f9bf4042..6875ee94 100644 --- a/examples/markdown/Cargo.toml +++ b/examples/markdown/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] +iced.features = ["highlighter", "debug"] pulldown-cmark = "0.11" diff --git a/examples/markdown/overview.md b/examples/markdown/overview.md new file mode 100644 index 00000000..ca3250f1 --- /dev/null +++ b/examples/markdown/overview.md @@ -0,0 +1,102 @@ +# Overview + +Inspired by [The Elm Architecture], Iced expects you to split user interfaces +into four different concepts: + +* __State__ — the state of your application +* __Messages__ — user interactions or meaningful events that you care + about +* __View logic__ — a way to display your __state__ as widgets that + may produce __messages__ on user interaction +* __Update logic__ — a way to react to __messages__ and update your + __state__ + +We can build something to see how this works! Let's say we want a simple counter +that can be incremented and decremented using two buttons. + +We start by modelling the __state__ of our application: + +```rust +#[derive(Default)] +struct Counter { + value: i32, +} +``` + +Next, we need to define the possible user interactions of our counter: +the button presses. These interactions are our __messages__: + +```rust +#[derive(Debug, Clone, Copy)] +pub enum Message { + Increment, + Decrement, +} +``` + +Now, let's show the actual counter by putting it all together in our +__view logic__: + +```rust +use iced::widget::{button, column, text, Column}; + +impl Counter { + pub fn view(&self) -> Column<Message> { + // We use a column: a simple vertical layout + column![ + // The increment button. We tell it to produce an + // `Increment` message when pressed + button("+").on_press(Message::Increment), + + // We show the value of the counter here + text(self.value).size(50), + + // The decrement button. We tell it to produce a + // `Decrement` message when pressed + button("-").on_press(Message::Decrement), + ] + } +} +``` + +Finally, we need to be able to react to any produced __messages__ and change our +__state__ accordingly in our __update logic__: + +```rust +impl Counter { + // ... + + pub fn update(&mut self, message: Message) { + match message { + Message::Increment => { + self.value += 1; + } + Message::Decrement => { + self.value -= 1; + } + } + } +} +``` + +And that's everything! We just wrote a whole user interface. Let's run it: + +```rust +fn main() -> iced::Result { + iced::run("A cool counter", Counter::update, Counter::view) +} +``` + +Iced will automatically: + + 1. Take the result of our __view logic__ and layout its widgets. + 1. Process events from our system and produce __messages__ for our + __update logic__. + 1. Draw the resulting user interface. + +Read the [book], the [documentation], and the [examples] to learn more! + +[book]: https://book.iced.rs/ +[documentation]: https://docs.rs/iced/ +[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples +[The Elm Architecture]: https://guide.elm-lang.org/architecture/ diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 43adaf72..384645fa 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -1,7 +1,6 @@ -use iced::font; -use iced::padding; use iced::widget::{ - self, column, container, rich_text, row, span, text_editor, + self, column, container, rich_text, row, scrollable, span, text, + text_editor, }; use iced::{Element, Fill, Font, Task, Theme}; @@ -13,6 +12,8 @@ pub fn main() -> iced::Result { struct Markdown { content: text_editor::Content, + items: Vec<Item>, + theme: Theme, } #[derive(Debug, Clone)] @@ -22,11 +23,15 @@ enum Message { impl Markdown { fn new() -> (Self, Task<Message>) { + const INITIAL_CONTENT: &str = include_str!("../overview.md"); + + let theme = Theme::TokyoNight; + ( Self { - content: text_editor::Content::with_text( - "# Markdown Editor\nType your Markdown here...", - ), + content: text_editor::Content::with_text(INITIAL_CONTENT), + items: parse(INITIAL_CONTENT, &theme).collect(), + theme, }, widget::focus_next(), ) @@ -34,7 +39,14 @@ impl Markdown { fn update(&mut self, message: Message) { match message { Message::Edit(action) => { + let is_edit = action.is_edit(); + self.content.perform(action); + + if is_edit { + self.items = + parse(&self.content.text(), &self.theme).collect(); + } } } } @@ -46,127 +58,225 @@ impl Markdown { .padding(10) .font(Font::MONOSPACE); - let preview = { - let markdown = self.content.text(); - let parser = pulldown_cmark::Parser::new(&markdown); + let preview = markdown(&self.items); - let mut strong = false; - let mut emphasis = false; - let mut heading = None; - let mut spans = Vec::new(); - - let items = parser.filter_map(|event| match event { - pulldown_cmark::Event::Start(tag) => match tag { - pulldown_cmark::Tag::Strong => { - strong = true; - None - } - pulldown_cmark::Tag::Emphasis => { - emphasis = true; - None - } - pulldown_cmark::Tag::Heading { level, .. } => { - heading = Some(level); - None - } - _ => None, - }, - pulldown_cmark::Event::End(tag) => match tag { - pulldown_cmark::TagEnd::Emphasis => { - emphasis = false; - None - } - pulldown_cmark::TagEnd::Strong => { - strong = false; - None - } - pulldown_cmark::TagEnd::Heading(_) => { - heading = None; - Some( - container(rich_text(spans.drain(..))) - .padding(padding::bottom(5)) - .into(), - ) - } - pulldown_cmark::TagEnd::Paragraph => Some( - container(rich_text(spans.drain(..))) - .padding(padding::bottom(15)) - .into(), - ), - pulldown_cmark::TagEnd::CodeBlock => Some( - container( - container( - rich_text(spans.drain(..)) - .font(Font::MONOSPACE), - ) - .width(Fill) - .padding(10) - .style(container::rounded_box), - ) - .padding(padding::bottom(15)) - .into(), - ), - _ => None, - }, - pulldown_cmark::Event::Text(text) => { - let span = span(text.into_string()); - - let span = match heading { - None => span, - Some(heading) => span.size(match heading { - pulldown_cmark::HeadingLevel::H1 => 32, - pulldown_cmark::HeadingLevel::H2 => 28, - pulldown_cmark::HeadingLevel::H3 => 24, - pulldown_cmark::HeadingLevel::H4 => 20, - pulldown_cmark::HeadingLevel::H5 => 16, - pulldown_cmark::HeadingLevel::H6 => 16, - }), - }; - - let span = if strong || emphasis { - span.font(Font { - weight: if strong { - font::Weight::Bold - } else { - font::Weight::Normal - }, - style: if emphasis { - font::Style::Italic - } else { - font::Style::Normal - }, - ..Font::default() - }) - } else { - span - }; - - spans.push(span); - - None - } - pulldown_cmark::Event::Code(code) => { - spans.push(span(code.into_string()).font(Font::MONOSPACE)); - None - } - pulldown_cmark::Event::SoftBreak => { - spans.push(span(" ")); - None - } - pulldown_cmark::Event::HardBreak => { - spans.push(span("\n")); - None - } - _ => None, - }); - - column(items).width(Fill) - }; - - row![editor, preview].spacing(10).padding(10).into() + row![ + editor, + scrollable(preview).spacing(10).width(Fill).height(Fill) + ] + .spacing(10) + .padding(10) + .into() } fn theme(&self) -> Theme { Theme::TokyoNight } } + +fn markdown<'a>( + items: impl IntoIterator<Item = &'a Item>, +) -> Element<'a, Message> { + use iced::padding; + + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { + Item::Heading(heading) => container(rich_text(heading)) + .padding(padding::top(if i > 0 { 8 } else { 0 })) + .into(), + Item::Paragraph(paragraph) => rich_text(paragraph).into(), + Item::List { start: None, items } => column( + items + .iter() + .map(|item| row!["•", rich_text(item)].spacing(10).into()), + ) + .spacing(10) + .into(), + Item::List { + start: Some(start), + items, + } => column(items.iter().enumerate().map(|(i, item)| { + row![text!("{}.", i as u64 + *start), rich_text(item)] + .spacing(10) + .into() + })) + .spacing(10) + .into(), + Item::CodeBlock(code) => { + container(rich_text(code).font(Font::MONOSPACE).size(12)) + .width(Fill) + .padding(10) + .style(container::rounded_box) + .into() + } + }); + + column(blocks).width(Fill).spacing(16).into() +} + +#[derive(Debug, Clone)] +enum Item { + Heading(Vec<text::Span<'static>>), + Paragraph(Vec<text::Span<'static>>), + CodeBlock(Vec<text::Span<'static>>), + List { + start: Option<u64>, + items: Vec<Vec<text::Span<'static>>>, + }, +} + +fn parse<'a>( + markdown: &'a str, + theme: &'a Theme, +) -> impl Iterator<Item = Item> + 'a { + use iced::font; + use iced::highlighter::{self, Highlighter}; + use text::Highlighter as _; + + let mut spans = Vec::new(); + let mut heading = None; + let mut strong = false; + let mut emphasis = false; + let mut link = false; + let mut list = Vec::new(); + let mut list_start = None; + let mut highlighter = None; + + let parser = pulldown_cmark::Parser::new(markdown); + + // We want to keep the `spans` capacity + #[allow(clippy::drain_collect)] + parser.filter_map(move |event| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Heading { level, .. } => { + heading = Some(level); + None + } + pulldown_cmark::Tag::Strong => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis => { + emphasis = true; + None + } + pulldown_cmark::Tag::Link { .. } => { + link = true; + None + } + pulldown_cmark::Tag::List(first_item) => { + list_start = first_item; + None + } + pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(language), + ) => { + highlighter = Some(Highlighter::new(&highlighter::Settings { + theme: highlighter::Theme::Base16Ocean, + token: language.to_string(), + })); + + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Heading(_) => { + heading = None; + Some(Item::Heading(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::Emphasis => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strong => { + strong = false; + None + } + pulldown_cmark::TagEnd::Link => { + link = false; + None + } + pulldown_cmark::TagEnd::Paragraph => { + Some(Item::Paragraph(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::List(_) => Some(Item::List { + start: list_start, + items: list.drain(..).collect(), + }), + pulldown_cmark::TagEnd::Item => { + list.push(spans.drain(..).collect()); + None + } + pulldown_cmark::TagEnd::CodeBlock => { + highlighter = None; + Some(Item::CodeBlock(spans.drain(..).collect())) + } + _ => None, + }, + pulldown_cmark::Event::Text(text) => { + if let Some(highlighter) = &mut highlighter { + for (range, highlight) in + highlighter.highlight_line(text.as_ref()) + { + let span = span(text[range].to_owned()) + .color_maybe(highlight.color()) + .font_maybe(highlight.font()); + + spans.push(span); + } + } else { + let span = span(text.into_string()); + + let span = match heading { + None => span, + Some(heading) => span.size(match heading { + pulldown_cmark::HeadingLevel::H1 => 32, + pulldown_cmark::HeadingLevel::H2 => 28, + pulldown_cmark::HeadingLevel::H3 => 24, + pulldown_cmark::HeadingLevel::H4 => 20, + pulldown_cmark::HeadingLevel::H5 => 16, + pulldown_cmark::HeadingLevel::H6 => 16, + }), + }; + + let span = if strong || emphasis { + span.font(Font { + weight: if strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = + span.color_maybe(link.then(|| theme.palette().primary)); + + spans.push(span); + } + + None + } + pulldown_cmark::Event::Code(code) => { + spans.push(span(code.into_string()).font(Font::MONOSPACE)); + None + } + pulldown_cmark::Event::SoftBreak => { + spans.push(span(" ")); + None + } + pulldown_cmark::Event::HardBreak => { + spans.push(span("\n")); + None + } + _ => None, + }) +} diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 7636a712..deee199f 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -1,8 +1,9 @@ //! A syntax highlighter for iced. use iced_core as core; +use crate::core::font::{self, Font}; use crate::core::text::highlighter::{self, Format}; -use crate::core::{Color, Font}; +use crate::core::Color; use once_cell::sync::Lazy; use std::ops::Range; @@ -35,7 +36,7 @@ impl highlighter::Highlighter for Highlighter { fn new(settings: &Self::Settings) -> Self { let syntax = SYNTAXES - .find_syntax_by_token(&settings.extension) + .find_syntax_by_token(&settings.token) .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); let highlighter = highlighting::Highlighter::new( @@ -55,7 +56,7 @@ impl highlighter::Highlighter for Highlighter { fn update(&mut self, new_settings: &Self::Settings) { self.syntax = SYNTAXES - .find_syntax_by_token(&new_settings.extension) + .find_syntax_by_token(&new_settings.token) .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); self.highlighter = highlighting::Highlighter::new( @@ -141,11 +142,11 @@ pub struct Settings { /// /// It dictates the color scheme that will be used for highlighting. pub theme: Theme, - /// The extension of the file to highlight. + /// The extension of the file or the name of the language to highlight. /// - /// The [`Highlighter`] will use the extension to automatically determine + /// The [`Highlighter`] will use the token to automatically determine /// the grammar to use for highlighting. - pub extension: String, + pub token: String, } /// A highlight produced by a [`Highlighter`]. @@ -166,7 +167,29 @@ impl Highlight { /// /// If `None`, the original font should be unchanged. pub fn font(&self) -> Option<Font> { - None + self.0.font_style.and_then(|style| { + let bold = style.contains(highlighting::FontStyle::BOLD); + + let italic = style.contains(highlighting::FontStyle::ITALIC); + + if bold || italic { + Some(Font { + weight: if bold { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if italic { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::MONOSPACE + }) + } else { + None + } + }) } /// Returns the [`Format`] of the [`Highlight`]. diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 66b37ccb..0390079f 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -24,7 +24,7 @@ use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; use crate::{Column, MouseArea, Row, Space, Stack, Themer}; -use std::borrow::Borrow; +use std::borrow::{Borrow, Cow}; use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. @@ -684,7 +684,7 @@ where /// /// [`Rich`]: text::Rich pub fn rich_text<'a, Theme, Renderer>( - spans: impl IntoIterator<Item = text::Span<'a, Renderer::Font>>, + spans: impl Into<Cow<'a, [text::Span<'a, Renderer::Font>]>>, ) -> text::Rich<'a, Theme, Renderer> where Theme: text::Catalog + 'a, diff --git a/widget/src/text.rs b/widget/src/text.rs index c32f9be1..9bf7fce4 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,7 +1,7 @@ //! Draw and interact with text. mod rich; -pub use crate::core::text::{Fragment, IntoFragment, Span}; +pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span}; pub use crate::core::widget::text::*; pub use rich::Rich; diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index dc784310..5c44ed9e 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -11,6 +11,8 @@ use crate::core::{ self, Color, Element, Length, Pixels, Rectangle, Size, Widget, }; +use std::borrow::Cow; + /// A bunch of [`Rich`] text. #[derive(Debug)] pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> @@ -18,7 +20,7 @@ where Theme: Catalog, Renderer: core::text::Renderer, { - spans: Vec<Span<'a, Renderer::Font>>, + spans: Cow<'a, [Span<'a, Renderer::Font>]>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -37,7 +39,7 @@ where /// Creates a new empty [`Rich`] text. pub fn new() -> Self { Self { - spans: Vec::new(), + spans: Cow::default(), size: None, line_height: LineHeight::default(), width: Length::Shrink, @@ -51,10 +53,10 @@ where /// Creates a new [`Rich`] text with the given text spans. pub fn with_spans( - spans: impl IntoIterator<Item = Span<'a, Renderer::Font>>, + spans: impl Into<Cow<'a, [Span<'a, Renderer::Font>]>>, ) -> Self { Self { - spans: spans.into_iter().collect(), + spans: spans.into(), ..Self::new() } } @@ -151,7 +153,7 @@ where /// Adds a new text [`Span`] to the [`Rich`] text. pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { - self.spans.push(span.into()); + self.spans.to_mut().push(span.into()); self } } @@ -207,7 +209,7 @@ where limits, self.width, self.height, - self.spans.as_slice(), + self.spans.as_ref(), self.line_height, self.size, self.font, @@ -303,6 +305,22 @@ where }) } +impl<'a, Theme, Renderer> FromIterator<Span<'a, Renderer::Font>> + for Rich<'a, Theme, Renderer> +where + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn from_iter<T: IntoIterator<Item = Span<'a, Renderer::Font>>>( + spans: T, + ) -> Self { + Self { + spans: spans.into_iter().collect(), + ..Self::new() + } + } +} + impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0156b960..e494a3b0 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -9,6 +9,7 @@ use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; +use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, @@ -338,6 +339,22 @@ impl<Highlighter: text::Highlighter> State<Highlighter> { } } +impl<Highlighter: text::Highlighter> operation::Focusable + for State<Highlighter> +{ + fn is_focused(&self) -> bool { + self.is_focused + } + + fn focus(&mut self) { + self.is_focused = true; + } + + fn unfocus(&mut self) { + self.is_focused = false; + } +} + impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer> where @@ -640,6 +657,18 @@ where mouse::Interaction::default() } } + + fn operate( + &self, + tree: &mut widget::Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn widget::Operation<()>, + ) { + let state = tree.state.downcast_mut::<State<Highlighter>>(); + + operation.focusable(state, None); + } } impl<'a, Highlighter, Message, Theme, Renderer> From aa62fa2ce992949d20ddbe8683ed2be0d922a568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 18 Jul 2024 13:22:53 +0200 Subject: [PATCH 121/657] Adapt `scrollable` sizing strategy to contents --- examples/markdown/src/main.rs | 11 ++++------- widget/src/scrollable.rs | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 384645fa..1e3769ff 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -60,13 +60,10 @@ impl Markdown { let preview = markdown(&self.items); - row![ - editor, - scrollable(preview).spacing(10).width(Fill).height(Fill) - ] - .spacing(10) - .padding(10) - .into() + row![editor, scrollable(preview).spacing(10).height(Fill)] + .spacing(10) + .padding(10) + .into() } fn theme(&self) -> Theme { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index b1082203..6dd593cb 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -62,19 +62,27 @@ where .validate() } - fn validate(self) -> Self { + fn validate(mut self) -> Self { + let size_hint = self.content.as_widget().size_hint(); + debug_assert!( - self.direction.vertical().is_none() - || !self.content.as_widget().size_hint().height.is_fill(), + self.direction.vertical().is_none() || !size_hint.height.is_fill(), "scrollable content must not fill its vertical scrolling axis" ); debug_assert!( - self.direction.horizontal().is_none() - || !self.content.as_widget().size_hint().width.is_fill(), + self.direction.horizontal().is_none() || !size_hint.width.is_fill(), "scrollable content must not fill its horizontal scrolling axis" ); + if self.direction.horizontal().is_none() { + self.width = self.width.enclose(size_hint.width); + } + + if self.direction.vertical().is_none() { + self.height = self.height.enclose(size_hint.height); + } + self } From 47b7a36f36b99e346909390621b04f6691ff46d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 18 Jul 2024 14:34:00 +0200 Subject: [PATCH 122/657] Create `markdown` widget helpers in `iced_widget` --- Cargo.toml | 5 +- examples/markdown/Cargo.toml | 4 +- examples/markdown/src/main.rs | 224 ++----------------------------- widget/Cargo.toml | 10 +- widget/src/helpers.rs | 5 + widget/src/lib.rs | 3 + widget/src/markdown.rs | 246 ++++++++++++++++++++++++++++++++++ 7 files changed, 277 insertions(+), 220 deletions(-) create mode 100644 widget/src/markdown.rs diff --git a/Cargo.toml b/Cargo.toml index bc566bf6..d301b36d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ svg = ["iced_widget/svg"] canvas = ["iced_widget/canvas"] # Enables the `QRCode` widget qr_code = ["iced_widget/qr_code"] +# Enables the `markdown` widget +markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) @@ -51,7 +53,7 @@ web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU webgl = ["iced_renderer/webgl"] # Enables the syntax `highlighter` module -highlighter = ["iced_highlighter"] +highlighter = ["iced_highlighter", "iced_widget/highlighter"] # Enables experimental multi-window support. multi-window = ["iced_winit/multi-window"] # Enables the advanced module @@ -155,6 +157,7 @@ num-traits = "0.2" once_cell = "1.0" ouroboros = "0.18" palette = "0.7" +pulldown-cmark = "0.11" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" resvg = "0.42" diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml index 6875ee94..9404d5d2 100644 --- a/examples/markdown/Cargo.toml +++ b/examples/markdown/Cargo.toml @@ -7,6 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["highlighter", "debug"] - -pulldown-cmark = "0.11" +iced.features = ["markdown", "highlighter", "debug"] diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 1e3769ff..28b5941f 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -1,7 +1,4 @@ -use iced::widget::{ - self, column, container, rich_text, row, scrollable, span, text, - text_editor, -}; +use iced::widget::{self, markdown, row, scrollable, text_editor}; use iced::{Element, Fill, Font, Task, Theme}; pub fn main() -> iced::Result { @@ -12,7 +9,7 @@ pub fn main() -> iced::Result { struct Markdown { content: text_editor::Content, - items: Vec<Item>, + items: Vec<markdown::Item>, theme: Theme, } @@ -30,7 +27,8 @@ impl Markdown { ( Self { content: text_editor::Content::with_text(INITIAL_CONTENT), - items: parse(INITIAL_CONTENT, &theme).collect(), + items: markdown::parse(INITIAL_CONTENT, theme.palette()) + .collect(), theme, }, widget::focus_next(), @@ -44,8 +42,11 @@ impl Markdown { self.content.perform(action); if is_edit { - self.items = - parse(&self.content.text(), &self.theme).collect(); + self.items = markdown::parse( + &self.content.text(), + self.theme.palette(), + ) + .collect(); } } } @@ -70,210 +71,3 @@ impl Markdown { Theme::TokyoNight } } - -fn markdown<'a>( - items: impl IntoIterator<Item = &'a Item>, -) -> Element<'a, Message> { - use iced::padding; - - let blocks = items.into_iter().enumerate().map(|(i, item)| match item { - Item::Heading(heading) => container(rich_text(heading)) - .padding(padding::top(if i > 0 { 8 } else { 0 })) - .into(), - Item::Paragraph(paragraph) => rich_text(paragraph).into(), - Item::List { start: None, items } => column( - items - .iter() - .map(|item| row!["•", rich_text(item)].spacing(10).into()), - ) - .spacing(10) - .into(), - Item::List { - start: Some(start), - items, - } => column(items.iter().enumerate().map(|(i, item)| { - row![text!("{}.", i as u64 + *start), rich_text(item)] - .spacing(10) - .into() - })) - .spacing(10) - .into(), - Item::CodeBlock(code) => { - container(rich_text(code).font(Font::MONOSPACE).size(12)) - .width(Fill) - .padding(10) - .style(container::rounded_box) - .into() - } - }); - - column(blocks).width(Fill).spacing(16).into() -} - -#[derive(Debug, Clone)] -enum Item { - Heading(Vec<text::Span<'static>>), - Paragraph(Vec<text::Span<'static>>), - CodeBlock(Vec<text::Span<'static>>), - List { - start: Option<u64>, - items: Vec<Vec<text::Span<'static>>>, - }, -} - -fn parse<'a>( - markdown: &'a str, - theme: &'a Theme, -) -> impl Iterator<Item = Item> + 'a { - use iced::font; - use iced::highlighter::{self, Highlighter}; - use text::Highlighter as _; - - let mut spans = Vec::new(); - let mut heading = None; - let mut strong = false; - let mut emphasis = false; - let mut link = false; - let mut list = Vec::new(); - let mut list_start = None; - let mut highlighter = None; - - let parser = pulldown_cmark::Parser::new(markdown); - - // We want to keep the `spans` capacity - #[allow(clippy::drain_collect)] - parser.filter_map(move |event| match event { - pulldown_cmark::Event::Start(tag) => match tag { - pulldown_cmark::Tag::Heading { level, .. } => { - heading = Some(level); - None - } - pulldown_cmark::Tag::Strong => { - strong = true; - None - } - pulldown_cmark::Tag::Emphasis => { - emphasis = true; - None - } - pulldown_cmark::Tag::Link { .. } => { - link = true; - None - } - pulldown_cmark::Tag::List(first_item) => { - list_start = first_item; - None - } - pulldown_cmark::Tag::CodeBlock( - pulldown_cmark::CodeBlockKind::Fenced(language), - ) => { - highlighter = Some(Highlighter::new(&highlighter::Settings { - theme: highlighter::Theme::Base16Ocean, - token: language.to_string(), - })); - - None - } - _ => None, - }, - pulldown_cmark::Event::End(tag) => match tag { - pulldown_cmark::TagEnd::Heading(_) => { - heading = None; - Some(Item::Heading(spans.drain(..).collect())) - } - pulldown_cmark::TagEnd::Emphasis => { - emphasis = false; - None - } - pulldown_cmark::TagEnd::Strong => { - strong = false; - None - } - pulldown_cmark::TagEnd::Link => { - link = false; - None - } - pulldown_cmark::TagEnd::Paragraph => { - Some(Item::Paragraph(spans.drain(..).collect())) - } - pulldown_cmark::TagEnd::List(_) => Some(Item::List { - start: list_start, - items: list.drain(..).collect(), - }), - pulldown_cmark::TagEnd::Item => { - list.push(spans.drain(..).collect()); - None - } - pulldown_cmark::TagEnd::CodeBlock => { - highlighter = None; - Some(Item::CodeBlock(spans.drain(..).collect())) - } - _ => None, - }, - pulldown_cmark::Event::Text(text) => { - if let Some(highlighter) = &mut highlighter { - for (range, highlight) in - highlighter.highlight_line(text.as_ref()) - { - let span = span(text[range].to_owned()) - .color_maybe(highlight.color()) - .font_maybe(highlight.font()); - - spans.push(span); - } - } else { - let span = span(text.into_string()); - - let span = match heading { - None => span, - Some(heading) => span.size(match heading { - pulldown_cmark::HeadingLevel::H1 => 32, - pulldown_cmark::HeadingLevel::H2 => 28, - pulldown_cmark::HeadingLevel::H3 => 24, - pulldown_cmark::HeadingLevel::H4 => 20, - pulldown_cmark::HeadingLevel::H5 => 16, - pulldown_cmark::HeadingLevel::H6 => 16, - }), - }; - - let span = if strong || emphasis { - span.font(Font { - weight: if strong { - font::Weight::Bold - } else { - font::Weight::Normal - }, - style: if emphasis { - font::Style::Italic - } else { - font::Style::Normal - }, - ..Font::default() - }) - } else { - span - }; - - let span = - span.color_maybe(link.then(|| theme.palette().primary)); - - spans.push(span); - } - - None - } - pulldown_cmark::Event::Code(code) => { - spans.push(span(code.into_string()).font(Font::MONOSPACE)); - None - } - pulldown_cmark::Event::SoftBreak => { - spans.push(span(" ")); - None - } - pulldown_cmark::Event::HardBreak => { - spans.push(span("\n")); - None - } - _ => None, - }) -} diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 498a768b..2f483b79 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -22,8 +22,10 @@ lazy = ["ouroboros"] image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] -qr_code = ["canvas", "qrcode"] +qr_code = ["canvas", "dep:qrcode"] wgpu = ["iced_renderer/wgpu"] +markdown = ["dep:pulldown-cmark"] +highlighter = ["dep:iced_highlighter"] advanced = [] [dependencies] @@ -41,3 +43,9 @@ ouroboros.optional = true qrcode.workspace = true qrcode.optional = true + +pulldown-cmark.workspace = true +pulldown-cmark.optional = true + +iced_highlighter.workspace = true +iced_highlighter.optional = true diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 0390079f..43fee845 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,6 +7,7 @@ use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; +use crate::markdown::{self}; use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; @@ -702,6 +703,10 @@ pub fn span<'a, Font>( text::Span::new(text) } +#[cfg(feature = "markdown")] +#[doc(inline)] +pub use markdown::view as markdown; + /// Creates a new [`Checkbox`]. /// /// [`Checkbox`]: crate::Checkbox diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 00e9aaa4..115a29e5 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -130,5 +130,8 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +#[cfg(feature = "markdown")] +pub mod markdown; + pub use crate::core::theme::{self, Theme}; pub use renderer::Renderer; diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs new file mode 100644 index 00000000..bbb5b463 --- /dev/null +++ b/widget/src/markdown.rs @@ -0,0 +1,246 @@ +//! Parse and display Markdown. +//! +//! You can enable the `highlighter` feature for syntax highligting +//! in code blocks. +//! +//! Only the variants of [`Item`] are currently supported. +use crate::core::font::{self, Font}; +use crate::core::padding; +use crate::core::theme::{self, Theme}; +use crate::core::{self, Element, Length}; +use crate::{column, container, rich_text, row, span, text}; + +/// A Markdown item. +#[derive(Debug, Clone)] +pub enum Item { + /// A heading. + Heading(Vec<text::Span<'static>>), + /// A paragraph. + Paragraph(Vec<text::Span<'static>>), + /// A code block. + /// + /// You can enable the `highlighter` feature for syntax highligting. + CodeBlock(Vec<text::Span<'static>>), + /// A list. + List { + /// The first number of the list, if it is ordered. + start: Option<u64>, + /// The items of the list. + items: Vec<Vec<text::Span<'static>>>, + }, +} + +/// Parse the given Markdown content. +pub fn parse( + markdown: &str, + palette: theme::Palette, +) -> impl Iterator<Item = Item> + '_ { + let mut spans = Vec::new(); + let mut heading = None; + let mut strong = false; + let mut emphasis = false; + let mut link = false; + let mut list = Vec::new(); + let mut list_start = None; + + #[cfg(feature = "highlighter")] + let mut highlighter = None; + + let parser = pulldown_cmark::Parser::new(markdown); + + // We want to keep the `spans` capacity + #[allow(clippy::drain_collect)] + parser.filter_map(move |event| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Heading { level, .. } => { + heading = Some(level); + None + } + pulldown_cmark::Tag::Strong => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis => { + emphasis = true; + None + } + pulldown_cmark::Tag::Link { .. } => { + link = true; + None + } + pulldown_cmark::Tag::List(first_item) => { + list_start = first_item; + None + } + pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(_language), + ) => { + #[cfg(feature = "highlighter")] + { + use iced_highlighter::{self, Highlighter}; + use text::Highlighter as _; + + highlighter = + Some(Highlighter::new(&iced_highlighter::Settings { + theme: iced_highlighter::Theme::Base16Ocean, + token: _language.to_string(), + })); + } + + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Heading(_) => { + heading = None; + Some(Item::Heading(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::Emphasis => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strong => { + strong = false; + None + } + pulldown_cmark::TagEnd::Link => { + link = false; + None + } + pulldown_cmark::TagEnd::Paragraph => { + Some(Item::Paragraph(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::List(_) => Some(Item::List { + start: list_start, + items: list.drain(..).collect(), + }), + pulldown_cmark::TagEnd::Item => { + list.push(spans.drain(..).collect()); + None + } + pulldown_cmark::TagEnd::CodeBlock => { + #[cfg(feature = "highlighter")] + { + highlighter = None; + } + + Some(Item::CodeBlock(spans.drain(..).collect())) + } + _ => None, + }, + pulldown_cmark::Event::Text(text) => { + #[cfg(feature = "highlighter")] + if let Some(highlighter) = &mut highlighter { + use text::Highlighter as _; + + for (range, highlight) in + highlighter.highlight_line(text.as_ref()) + { + let span = span(text[range].to_owned()) + .color_maybe(highlight.color()) + .font_maybe(highlight.font()); + + spans.push(span); + } + + return None; + } + + let span = span(text.into_string()); + + let span = match heading { + None => span, + Some(heading) => span.size(match heading { + pulldown_cmark::HeadingLevel::H1 => 32, + pulldown_cmark::HeadingLevel::H2 => 28, + pulldown_cmark::HeadingLevel::H3 => 24, + pulldown_cmark::HeadingLevel::H4 => 20, + pulldown_cmark::HeadingLevel::H5 => 16, + pulldown_cmark::HeadingLevel::H6 => 16, + }), + }; + + let span = if strong || emphasis { + span.font(Font { + weight: if strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = span.color_maybe(link.then_some(palette.primary)); + + spans.push(span); + + None + } + pulldown_cmark::Event::Code(code) => { + spans.push(span(code.into_string()).font(Font::MONOSPACE)); + None + } + pulldown_cmark::Event::SoftBreak => { + spans.push(span(" ")); + None + } + pulldown_cmark::Event::HardBreak => { + spans.push(span("\n")); + None + } + _ => None, + }) +} + +/// Display a bunch of Markdown items. +/// +/// You can obtain the items with [`parse`]. +pub fn view<'a, Message, Renderer>( + items: impl IntoIterator<Item = &'a Item>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { + Item::Heading(heading) => container(rich_text(heading)) + .padding(padding::top(if i > 0 { 8 } else { 0 })) + .into(), + Item::Paragraph(paragraph) => rich_text(paragraph).into(), + Item::List { start: None, items } => column( + items + .iter() + .map(|item| row!["•", rich_text(item)].spacing(10).into()), + ) + .spacing(10) + .into(), + Item::List { + start: Some(start), + items, + } => column(items.iter().enumerate().map(|(i, item)| { + row![text!("{}.", i as u64 + *start), rich_text(item)] + .spacing(10) + .into() + })) + .spacing(10) + .into(), + Item::CodeBlock(code) => { + container(rich_text(code).font(Font::MONOSPACE).size(12)) + .width(Length::Fill) + .padding(10) + .style(container::rounded_box) + .into() + } + }); + + Element::new(column(blocks).width(Length::Fill).spacing(16)) +} From 06dc507beba311f0862a0619285dc3d97348fd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 18 Jul 2024 14:54:26 +0200 Subject: [PATCH 123/657] Fix `markdown` import in `iced_widget` --- widget/src/helpers.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 43fee845..aa9394cb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,7 +7,6 @@ use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; -use crate::markdown::{self}; use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; @@ -705,7 +704,7 @@ pub fn span<'a, Font>( #[cfg(feature = "markdown")] #[doc(inline)] -pub use markdown::view as markdown; +pub use crate::markdown::view as markdown; /// Creates a new [`Checkbox`]. /// From 06acb740fba1889c6a9fb48dfa3ae3aaac1df3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 18 Jul 2024 15:14:54 +0200 Subject: [PATCH 124/657] Return proper `theme` in `markdown` example --- examples/markdown/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 28b5941f..d902a4f7 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -68,6 +68,6 @@ impl Markdown { } fn theme(&self) -> Theme { - Theme::TokyoNight + self.theme.clone() } } From 1d1a5f1a28ba3002fcdd64e4cbfe1881d7ae37cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 18 Jul 2024 22:55:40 +0200 Subject: [PATCH 125/657] Fix newlines in `highlighter` and `markdown` example --- examples/markdown/src/main.rs | 1 + highlighter/src/lib.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index d902a4f7..6b7adc12 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -34,6 +34,7 @@ impl Markdown { widget::focus_next(), ) } + fn update(&mut self, message: Message) { match message { Message::Edit(action) => { diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index deee199f..83a15cb1 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -169,7 +169,6 @@ impl Highlight { pub fn font(&self) -> Option<Font> { self.0.font_style.and_then(|style| { let bold = style.contains(highlighting::FontStyle::BOLD); - let italic = style.contains(highlighting::FontStyle::ITALIC); if bold || italic { From c851e67734ec0c761adfd7881c576856ea38734b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector0193@gmail.com> Date: Fri, 19 Jul 2024 00:59:54 +0200 Subject: [PATCH 126/657] Fix `text::State` downcast in some widgets --- core/src/widget/text.rs | 2 +- widget/src/checkbox.rs | 4 +++- widget/src/radio.rs | 4 +++- widget/src/toggler.rs | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index d0ecd27b..5c5b78dd 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -154,7 +154,7 @@ where /// The internal state of a [`Text`] widget. #[derive(Debug, Default)] -pub struct State<P: Paragraph>(paragraph::Plain<P>); +pub struct State<P: Paragraph>(pub paragraph::Plain<P>); impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Text<'a, Theme, Renderer> diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 225c316d..e5abfbb4 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -358,12 +358,14 @@ where { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style { color: style.text_color, }, diff --git a/widget/src/radio.rs b/widget/src/radio.rs index ccc6a21e..536a7483 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -353,12 +353,14 @@ where { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style { color: style.text_color, }, diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 853d27ac..821e2526 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -289,12 +289,14 @@ where if self.label.is_some() { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, style, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style::default(), viewport, ); From 05884870fcca61849d8aa35cade354a192621092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 20 Jul 2024 15:53:50 +0200 Subject: [PATCH 127/657] Make `container::Style` API more consistent --- examples/layout/src/main.rs | 5 +++-- widget/src/container.rs | 38 ++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index d0827fad..f39e24f9 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,3 +1,4 @@ +use iced::border; use iced::keyboard; use iced::mouse; use iced::widget::{ @@ -85,7 +86,7 @@ impl Layout { let palette = theme.extended_palette(); container::Style::default() - .with_border(palette.background.strong.color, 4.0) + .border(border::color(palette.background.strong.color).width(4)) }) .padding(4); @@ -240,7 +241,7 @@ fn application<'a>() -> Element<'a, Message> { let palette = theme.extended_palette(); container::Style::default() - .with_border(palette.background.strong.color, 1) + .border(border::color(palette.background.strong.color).width(1)) }); let sidebar = container( diff --git a/widget/src/container.rs b/widget/src/container.rs index 5680bc30..d65bb086 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -546,46 +546,54 @@ pub struct Style { } impl Style { - /// Updates the border of the [`Style`] with the given [`Color`] and `width`. - pub fn with_border( - self, - color: impl Into<Color>, - width: impl Into<Pixels>, - ) -> Self { + /// Updates the text color of the [`Style`]. + pub fn color(self, color: impl Into<Color>) -> Self { Self { - border: Border { - color: color.into(), - width: width.into().0, - ..Border::default() - }, + text_color: Some(color.into()), + ..self + } + } + + /// Updates the border of the [`Style`]. + pub fn border(self, border: impl Into<Border>) -> Self { + Self { + border: border.into(), ..self } } /// Updates the background of the [`Style`]. - pub fn with_background(self, background: impl Into<Background>) -> Self { + pub fn background(self, background: impl Into<Background>) -> Self { Self { background: Some(background.into()), ..self } } + + /// Updates the shadow of the [`Style`]. + pub fn shadow(self, shadow: impl Into<Shadow>) -> Self { + Self { + shadow: shadow.into(), + ..self + } + } } impl From<Color> for Style { fn from(color: Color) -> Self { - Self::default().with_background(color) + Self::default().background(color) } } impl From<Gradient> for Style { fn from(gradient: Gradient) -> Self { - Self::default().with_background(gradient) + Self::default().background(gradient) } } impl From<gradient::Linear> for Style { fn from(gradient: gradient::Linear) -> Self { - Self::default().with_background(gradient) + Self::default().background(gradient) } } From 58f361d6806084c121c72e8752274c1cdf72fefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 20 Jul 2024 15:54:02 +0200 Subject: [PATCH 128/657] Introduce `container::dark` style --- widget/src/container.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index d65bb086..b991e0f1 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -10,8 +10,9 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - self, Background, Clipboard, Color, Element, Layout, Length, Padding, - Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, + self, color, Background, Clipboard, Color, Element, Layout, Length, + Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, + Widget, }; use crate::runtime::task::{self, Task}; @@ -654,3 +655,12 @@ pub fn bordered_box(theme: &Theme) -> Style { ..Style::default() } } + +/// A [`Container`] with a dark background and white text. +pub fn dark(_theme: &Theme) -> Style { + Style { + background: Some(color!(0x333333).into()), + text_color: Some(Color::WHITE), + ..Style::default() + } +} From 7b0945729a23b90b05a26cd7c10a6a7ba1a46f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 20 Jul 2024 15:57:58 +0200 Subject: [PATCH 129/657] Make `container::dark` darker and rounded --- widget/src/container.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index b991e0f1..9224f2ce 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -659,8 +659,9 @@ pub fn bordered_box(theme: &Theme) -> Style { /// A [`Container`] with a dark background and white text. pub fn dark(_theme: &Theme) -> Style { Style { - background: Some(color!(0x333333).into()), + background: Some(color!(0x111111).into()), text_color: Some(Color::WHITE), + border: border::rounded(2), ..Style::default() } } From 68c8d913efd692dabe08f483cd02c1a93af671b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 20 Jul 2024 19:00:54 +0200 Subject: [PATCH 130/657] Support nested lists in `markdown` widget --- widget/src/markdown.rs | 134 ++++++++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 34 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index bbb5b463..de691a4d 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -26,7 +26,7 @@ pub enum Item { /// The first number of the list, if it is ordered. start: Option<u64>, /// The items of the list. - items: Vec<Vec<text::Span<'static>>>, + items: Vec<Vec<Item>>, }, } @@ -35,46 +35,83 @@ pub fn parse( markdown: &str, palette: theme::Palette, ) -> impl Iterator<Item = Item> + '_ { + struct List { + start: Option<u64>, + items: Vec<Vec<Item>>, + } + let mut spans = Vec::new(); let mut heading = None; let mut strong = false; let mut emphasis = false; + let mut metadata = false; + let mut table = false; let mut link = false; - let mut list = Vec::new(); - let mut list_start = None; + let mut lists = Vec::new(); #[cfg(feature = "highlighter")] let mut highlighter = None; - let parser = pulldown_cmark::Parser::new(markdown); + let parser = pulldown_cmark::Parser::new_ext( + markdown, + pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS + | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS + | pulldown_cmark::Options::ENABLE_TABLES, + ); + + let produce = |lists: &mut Vec<List>, item| { + if lists.is_empty() { + Some(item) + } else { + lists + .last_mut() + .expect("list context") + .items + .last_mut() + .expect("item context") + .push(item); + + None + } + }; // We want to keep the `spans` capacity #[allow(clippy::drain_collect)] parser.filter_map(move |event| match event { pulldown_cmark::Event::Start(tag) => match tag { - pulldown_cmark::Tag::Heading { level, .. } => { + pulldown_cmark::Tag::Heading { level, .. } + if !metadata && !table => + { heading = Some(level); None } - pulldown_cmark::Tag::Strong => { + pulldown_cmark::Tag::Strong if !metadata && !table => { strong = true; None } - pulldown_cmark::Tag::Emphasis => { + pulldown_cmark::Tag::Emphasis if !metadata && !table => { emphasis = true; None } - pulldown_cmark::Tag::Link { .. } => { + pulldown_cmark::Tag::Link { .. } if !metadata && !table => { link = true; None } - pulldown_cmark::Tag::List(first_item) => { - list_start = first_item; + pulldown_cmark::Tag::List(first_item) if !metadata && !table => { + lists.push(List { + start: first_item, + items: Vec::new(), + }); + + None + } + pulldown_cmark::Tag::Item => { + lists.last_mut().expect("List").items.push(Vec::new()); None } pulldown_cmark::Tag::CodeBlock( pulldown_cmark::CodeBlockKind::Fenced(_language), - ) => { + ) if !metadata && !table => { #[cfg(feature = "highlighter")] { use iced_highlighter::{self, Highlighter}; @@ -89,47 +126,76 @@ pub fn parse( None } + pulldown_cmark::Tag::MetadataBlock(_) => { + metadata = true; + None + } + pulldown_cmark::Tag::Table(_) => { + table = true; + None + } _ => None, }, pulldown_cmark::Event::End(tag) => match tag { - pulldown_cmark::TagEnd::Heading(_) => { + pulldown_cmark::TagEnd::Heading(_) if !metadata && !table => { heading = None; - Some(Item::Heading(spans.drain(..).collect())) + produce(&mut lists, Item::Heading(spans.drain(..).collect())) } - pulldown_cmark::TagEnd::Emphasis => { + pulldown_cmark::TagEnd::Emphasis if !metadata && !table => { emphasis = false; None } - pulldown_cmark::TagEnd::Strong => { + pulldown_cmark::TagEnd::Strong if !metadata && !table => { strong = false; None } - pulldown_cmark::TagEnd::Link => { + pulldown_cmark::TagEnd::Link if !metadata && !table => { link = false; None } - pulldown_cmark::TagEnd::Paragraph => { - Some(Item::Paragraph(spans.drain(..).collect())) + pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { + produce(&mut lists, Item::Paragraph(spans.drain(..).collect())) } - pulldown_cmark::TagEnd::List(_) => Some(Item::List { - start: list_start, - items: list.drain(..).collect(), - }), - pulldown_cmark::TagEnd::Item => { - list.push(spans.drain(..).collect()); - None + pulldown_cmark::TagEnd::Item if !metadata && !table => { + if spans.is_empty() { + None + } else { + produce( + &mut lists, + Item::Paragraph(spans.drain(..).collect()), + ) + } } - pulldown_cmark::TagEnd::CodeBlock => { + pulldown_cmark::TagEnd::List(_) if !metadata && !table => { + let list = lists.pop().expect("List"); + + produce( + &mut lists, + Item::List { + start: list.start, + items: list.items, + }, + ) + } + pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => { #[cfg(feature = "highlighter")] { highlighter = None; } - Some(Item::CodeBlock(spans.drain(..).collect())) + produce(&mut lists, Item::CodeBlock(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::MetadataBlock(_) => { + metadata = false; + None + } + pulldown_cmark::TagEnd::Table => { + table = false; + None } _ => None, }, - pulldown_cmark::Event::Text(text) => { + pulldown_cmark::Event::Text(text) if !metadata && !table => { #[cfg(feature = "highlighter")] if let Some(highlighter) = &mut highlighter { use text::Highlighter as _; @@ -185,15 +251,15 @@ pub fn parse( None } - pulldown_cmark::Event::Code(code) => { + pulldown_cmark::Event::Code(code) if !metadata && !table => { spans.push(span(code.into_string()).font(Font::MONOSPACE)); None } - pulldown_cmark::Event::SoftBreak => { + pulldown_cmark::Event::SoftBreak if !metadata && !table => { spans.push(span(" ")); None } - pulldown_cmark::Event::HardBreak => { + pulldown_cmark::Event::HardBreak if !metadata && !table => { spans.push(span("\n")); None } @@ -219,15 +285,15 @@ where Item::List { start: None, items } => column( items .iter() - .map(|item| row!["•", rich_text(item)].spacing(10).into()), + .map(|items| row!["•", view(items)].spacing(10).into()), ) .spacing(10) .into(), Item::List { start: Some(start), items, - } => column(items.iter().enumerate().map(|(i, item)| { - row![text!("{}.", i as u64 + *start), rich_text(item)] + } => column(items.iter().enumerate().map(|(i, items)| { + row![text!("{}.", i as u64 + *start), view(items)] .spacing(10) .into() })) From 4b44079f34aa9e01977a7974e5f49ae79ff6cd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 20 Jul 2024 19:54:25 +0200 Subject: [PATCH 131/657] Use `from_iter` in `rich_text!` macro --- widget/src/helpers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index aa9394cb..6def61d5 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -121,7 +121,7 @@ macro_rules! rich_text { $crate::Column::new() ); ($($x:expr),+ $(,)?) => ( - $crate::text::Rich::with_spans([$($crate::text::Span::from($x)),+]) + $crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+]) ); } From 9bfaf2840cffe35d689bd115a308d21961ab082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 12:45:05 +0200 Subject: [PATCH 132/657] Add `Link` support to `rich_text` widget --- core/src/renderer/null.rs | 8 +- core/src/text.rs | 34 +++++- core/src/text/paragraph.rs | 9 +- examples/markdown/Cargo.toml | 2 + examples/markdown/src/main.rs | 6 +- graphics/src/text/paragraph.rs | 42 ++++++-- widget/src/helpers.rs | 7 +- widget/src/markdown.rs | 68 +++++++----- widget/src/text/rich.rs | 182 ++++++++++++++++++++++++++++----- 9 files changed, 287 insertions(+), 71 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index f9d1a5b0..7aa3aafb 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -77,8 +77,8 @@ impl text::Paragraph for () { fn with_text(_text: Text<&str>) -> Self {} - fn with_spans( - _text: Text<&[text::Span<'_, Self::Font>], Self::Font>, + fn with_spans<Link>( + _text: Text<&[text::Span<'_, Link, Self::Font>], Self::Font>, ) -> Self { } @@ -107,6 +107,10 @@ impl text::Paragraph for () { fn hit_test(&self, _point: Point) -> Option<text::Hit> { None } + + fn hit_span(&self, _point: Point) -> Option<usize> { + None + } } impl text::Editor for () { diff --git a/core/src/text.rs b/core/src/text.rs index 22cfce13..c22734f8 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -223,8 +223,8 @@ pub trait Renderer: crate::Renderer { } /// A span of text. -#[derive(Debug, Clone, PartialEq)] -pub struct Span<'a, Font = crate::Font> { +#[derive(Debug, Clone)] +pub struct Span<'a, Link = (), Font = crate::Font> { /// The [`Fragment`] of text. pub text: Fragment<'a>, /// The size of the [`Span`] in [`Pixels`]. @@ -235,9 +235,11 @@ pub struct Span<'a, Font = crate::Font> { pub font: Option<Font>, /// The [`Color`] of the [`Span`]. pub color: Option<Color>, + /// The link of the [`Span`]. + pub link: Option<Link>, } -impl<'a, Font> Span<'a, Font> { +impl<'a, Link, Font> Span<'a, Link, Font> { /// Creates a new [`Span`] of text with the given text fragment. pub fn new(fragment: impl IntoFragment<'a>) -> Self { Self { @@ -246,6 +248,7 @@ impl<'a, Font> Span<'a, Font> { line_height: None, font: None, color: None, + link: None, } } @@ -285,14 +288,27 @@ impl<'a, Font> Span<'a, Font> { self } + /// Sets the link of the [`Span`]. + pub fn link(mut self, link: impl Into<Link>) -> Self { + self.link = Some(link.into()); + self + } + + /// Sets the link of the [`Span`], if any. + pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self { + self.link = link.map(Into::into); + self + } + /// Turns the [`Span`] into a static one. - pub fn to_static(self) -> Span<'static, Font> { + pub fn to_static(self) -> Span<'static, Link, Font> { Span { text: Cow::Owned(self.text.into_owned()), size: self.size, line_height: self.line_height, font: self.font, color: self.color, + link: self.link, } } } @@ -303,6 +319,16 @@ impl<'a, Font> From<&'a str> for Span<'a, Font> { } } +impl<'a, Link, Font: PartialEq> PartialEq for Span<'a, Link, Font> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + && self.size == other.size + && self.line_height == other.line_height + && self.font == other.font + && self.color == other.color + } +} + /// A fragment of [`Text`]. /// /// This is just an alias to a string that may be either diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 4ee83798..26650793 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -12,7 +12,9 @@ pub trait Paragraph: Sized + Default { fn with_text(text: Text<&str, Self::Font>) -> Self; /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn with_spans(text: Text<&[Span<'_, Self::Font>], Self::Font>) -> Self; + fn with_spans<Link>( + text: Text<&[Span<'_, Link, Self::Font>], Self::Font>, + ) -> Self; /// Lays out the [`Paragraph`] with some new boundaries. fn resize(&mut self, new_bounds: Size); @@ -35,6 +37,11 @@ pub trait Paragraph: Sized + Default { /// [`Paragraph`], returning information about the nearest character. fn hit_test(&self, point: Point) -> Option<Hit>; + /// Tests whether the provided point is within the boundaries of a + /// [`Span`] in the [`Paragraph`], returning the index of the [`Span`] + /// that was hit. + fn hit_span(&self, point: Point) -> Option<usize>; + /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml index 9404d5d2..cb74b954 100644 --- a/examples/markdown/Cargo.toml +++ b/examples/markdown/Cargo.toml @@ -8,3 +8,5 @@ publish = false [dependencies] iced.workspace = true iced.features = ["markdown", "highlighter", "debug"] + +open = "5.3" diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 6b7adc12..bb6eb57b 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -16,6 +16,7 @@ struct Markdown { #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), + LinkClicked(String), } impl Markdown { @@ -50,6 +51,9 @@ impl Markdown { .collect(); } } + Message::LinkClicked(link) => { + let _ = open::that(link); + } } } @@ -60,7 +64,7 @@ impl Markdown { .padding(10) .font(Font::MONOSPACE); - let preview = markdown(&self.items); + let preview = markdown(&self.items, Message::LinkClicked); row![editor, scrollable(preview).spacing(10).height(Fill)] .spacing(10) diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 37fa97f2..b69110b2 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -100,8 +100,8 @@ impl core::text::Paragraph for Paragraph { })) } - fn with_spans(text: Text<&[Span<'_>]>) -> Self { - log::trace!("Allocating rich paragraph: {:?}", text.content); + fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self { + log::trace!("Allocating rich paragraph: {} spans", text.content.len()); let mut font_system = text::font_system().write().expect("Write font system"); @@ -122,11 +122,9 @@ impl core::text::Paragraph for Paragraph { buffer.set_rich_text( font_system.raw(), - text.content.iter().map(|span| { - let attrs = cosmic_text::Attrs::new(); - + text.content.iter().enumerate().map(|(i, span)| { let attrs = if let Some(font) = span.font { - attrs + cosmic_text::Attrs::new() .family(text::to_family(font.family)) .weight(text::to_weight(font.weight)) .stretch(text::to_stretch(font.stretch)) @@ -156,7 +154,7 @@ impl core::text::Paragraph for Paragraph { attrs }; - (span.text.as_ref(), attrs) + (span.text.as_ref(), attrs.metadata(i)) }), text::to_attributes(text.font), text::to_shaping(text.shaping), @@ -231,6 +229,36 @@ impl core::text::Paragraph for Paragraph { Some(Hit::CharOffset(cursor.index)) } + fn hit_span(&self, point: Point) -> Option<usize> { + let internal = self.internal(); + + let cursor = internal.buffer.hit(point.x, point.y)?; + let line = internal.buffer.lines.get(cursor.line)?; + + let mut last_glyph = None; + let mut glyphs = line + .layout_opt() + .as_ref()? + .iter() + .flat_map(|line| line.glyphs.iter()) + .peekable(); + + while let Some(glyph) = glyphs.peek() { + if glyph.start <= cursor.index && cursor.index < glyph.end { + break; + } + + last_glyph = glyphs.next(); + } + + let glyph = match cursor.affinity { + cosmic_text::Affinity::Before => last_glyph, + cosmic_text::Affinity::After => glyphs.next(), + }?; + + Some(glyph.metadata) + } + fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { use unicode_segmentation::UnicodeSegmentation; diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 6def61d5..5b1cb5bc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -683,10 +683,11 @@ where /// Creates a new [`Rich`] text widget with the provided spans. /// /// [`Rich`]: text::Rich -pub fn rich_text<'a, Theme, Renderer>( - spans: impl Into<Cow<'a, [text::Span<'a, Renderer::Font>]>>, -) -> text::Rich<'a, Theme, Renderer> +pub fn rich_text<'a, Message, Link, Theme, Renderer>( + spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>, +) -> text::Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone, Theme: text::Catalog + 'a, Renderer: core::text::Renderer, { diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index de691a4d..dc207a34 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -14,13 +14,13 @@ use crate::{column, container, rich_text, row, span, text}; #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(Vec<text::Span<'static>>), + Heading(Vec<text::Span<'static, String>>), /// A paragraph. - Paragraph(Vec<text::Span<'static>>), + Paragraph(Vec<text::Span<'static, String>>), /// A code block. /// /// You can enable the `highlighter` feature for syntax highligting. - CodeBlock(Vec<text::Span<'static>>), + CodeBlock(Vec<text::Span<'static, String>>), /// A list. List { /// The first number of the list, if it is ordered. @@ -46,7 +46,7 @@ pub fn parse( let mut emphasis = false; let mut metadata = false; let mut table = false; - let mut link = false; + let mut link = None; let mut lists = Vec::new(); #[cfg(feature = "highlighter")] @@ -93,8 +93,10 @@ pub fn parse( emphasis = true; None } - pulldown_cmark::Tag::Link { .. } if !metadata && !table => { - link = true; + pulldown_cmark::Tag::Link { dest_url, .. } + if !metadata && !table => + { + link = Some(dest_url); None } pulldown_cmark::Tag::List(first_item) if !metadata && !table => { @@ -150,7 +152,7 @@ pub fn parse( None } pulldown_cmark::TagEnd::Link if !metadata && !table => { - link = false; + link = None; None } pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { @@ -245,7 +247,11 @@ pub fn parse( span }; - let span = span.color_maybe(link.then_some(palette.primary)); + let span = if let Some(link) = link.as_ref() { + span.color(palette.primary).link(link.to_string()) + } else { + span + }; spans.push(span); @@ -272,40 +278,48 @@ pub fn parse( /// You can obtain the items with [`parse`]. pub fn view<'a, Message, Renderer>( items: impl IntoIterator<Item = &'a Item>, + on_link_click: impl Fn(String) -> Message + Copy + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, Renderer: core::text::Renderer<Font = Font> + 'a, { let blocks = items.into_iter().enumerate().map(|(i, item)| match item { - Item::Heading(heading) => container(rich_text(heading)) - .padding(padding::top(if i > 0 { 8 } else { 0 })) - .into(), - Item::Paragraph(paragraph) => rich_text(paragraph).into(), - Item::List { start: None, items } => column( - items - .iter() - .map(|items| row!["•", view(items)].spacing(10).into()), - ) - .spacing(10) - .into(), + Item::Heading(heading) => { + container(rich_text(heading).on_link_click(on_link_click)) + .padding(padding::top(if i > 0 { 8 } else { 0 })) + .into() + } + Item::Paragraph(paragraph) => { + rich_text(paragraph).on_link_click(on_link_click).into() + } + Item::List { start: None, items } => { + column(items.iter().map(|items| { + row!["•", view(items, on_link_click)].spacing(10).into() + })) + .spacing(10) + .into() + } Item::List { start: Some(start), items, } => column(items.iter().enumerate().map(|(i, items)| { - row![text!("{}.", i as u64 + *start), view(items)] + row![text!("{}.", i as u64 + *start), view(items, on_link_click)] .spacing(10) .into() })) .spacing(10) .into(), - Item::CodeBlock(code) => { - container(rich_text(code).font(Font::MONOSPACE).size(12)) - .width(Length::Fill) - .padding(10) - .style(container::rounded_box) - .into() - } + Item::CodeBlock(code) => container( + rich_text(code) + .font(Font::MONOSPACE) + .size(12) + .on_link_click(on_link_click), + ) + .width(Length::Fill) + .padding(10) + .style(container::rounded_box) + .into(), }); Element::new(column(blocks).width(Length::Fill).spacing(16)) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 5c44ed9e..a44775c6 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -1,5 +1,6 @@ use crate::core::alignment; -use crate::core::layout::{self, Layout}; +use crate::core::event; +use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text::{Paragraph, Span}; @@ -8,19 +9,26 @@ use crate::core::widget::text::{ }; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Color, Element, Length, Pixels, Rectangle, Size, Widget, + self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, + Shell, Size, Widget, }; use std::borrow::Cow; /// A bunch of [`Rich`] text. -#[derive(Debug)] -pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> -where +#[allow(missing_debug_implementations)] +pub struct Rich< + 'a, + Message, + Link = (), + Theme = crate::Theme, + Renderer = crate::Renderer, +> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { - spans: Cow<'a, [Span<'a, Renderer::Font>]>, + spans: Cow<'a, [Span<'a, Link, Renderer::Font>]>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -29,10 +37,13 @@ where align_x: alignment::Horizontal, align_y: alignment::Vertical, class: Theme::Class<'a>, + on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>, } -impl<'a, Theme, Renderer> Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> + Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { @@ -48,12 +59,13 @@ where align_x: alignment::Horizontal::Left, align_y: alignment::Vertical::Top, class: Theme::default(), + on_link_click: None, } } /// Creates a new [`Rich`] text with the given text spans. pub fn with_spans( - spans: impl Into<Cow<'a, [Span<'a, Renderer::Font>]>>, + spans: impl Into<Cow<'a, [Span<'a, Link, Renderer::Font>]>>, ) -> Self { Self { spans: spans.into(), @@ -143,6 +155,15 @@ where self.style(move |_theme| Style { color }) } + /// Sets the message handler for link clicks on the [`Rich`] text. + pub fn on_link_click( + mut self, + on_link_click: impl Fn(Link) -> Message + 'a, + ) -> Self { + self.on_link_click = Some(Box::new(on_link_click)); + self + } + /// Sets the default style class of the [`Rich`] text. #[cfg(feature = "advanced")] #[must_use] @@ -152,14 +173,19 @@ where } /// Adds a new text [`Span`] to the [`Rich`] text. - pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { + pub fn push( + mut self, + span: impl Into<Span<'a, Link, Renderer::Font>>, + ) -> Self { self.spans.to_mut().push(span.into()); self } } -impl<'a, Theme, Renderer> Default for Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> Default + for Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { @@ -168,24 +194,27 @@ where } } -struct State<P: Paragraph> { - spans: Vec<Span<'static, P::Font>>, +struct State<Link, P: Paragraph> { + spans: Vec<Span<'static, Link, P::Font>>, + span_pressed: Option<usize>, paragraph: P, } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<State<Renderer::Paragraph>>() + tree::Tag::of::<State<Link, Renderer::Paragraph>>() } fn state(&self) -> tree::State { - tree::State::new(State { + tree::State::new(State::<Link, _> { spans: Vec::new(), + span_pressed: None, paragraph: Renderer::Paragraph::default(), }) } @@ -204,7 +233,8 @@ where limits: &layout::Limits, ) -> layout::Node { layout( - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), + tree.state + .downcast_mut::<State<Link, Renderer::Paragraph>>(), renderer, limits, self.width, @@ -228,7 +258,10 @@ where _cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + let style = theme.style(&self.class); text::draw( @@ -240,15 +273,106 @@ where viewport, ); } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let Some(on_link_click) = self.on_link_click.as_ref() else { + return event::Status::Ignored; + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if let Some(span) = state.paragraph.hit_span(position) { + state.span_pressed = Some(span); + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if let Some(span_pressed) = state.span_pressed { + state.span_pressed = None; + + if let Some(position) = cursor.position_in(layout.bounds()) + { + match state.paragraph.hit_span(position) { + Some(span) if span == span_pressed => { + if let Some(link) = state + .spans + .get(span) + .and_then(|span| span.link.clone()) + { + shell.publish(on_link_click(link)); + } + } + _ => {} + } + } + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if self.on_link_click.is_none() { + return mouse::Interaction::None; + } + + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + + if let Some(span) = state + .paragraph + .hit_span(position) + .and_then(|span| state.spans.get(span)) + { + if span.link.is_some() { + return mouse::Interaction::Pointer; + } + } + } + + mouse::Interaction::None + } } -fn layout<Renderer>( - state: &mut State<Renderer::Paragraph>, +fn layout<Link, Renderer>( + state: &mut State<Link, Renderer::Paragraph>, renderer: &Renderer, limits: &layout::Limits, width: Length, height: Length, - spans: &[Span<'_, Renderer::Font>], + spans: &[Span<'_, Link, Renderer::Font>], line_height: LineHeight, size: Option<Pixels>, font: Option<Renderer::Font>, @@ -256,6 +380,7 @@ fn layout<Renderer>( vertical_alignment: alignment::Vertical, ) -> layout::Node where + Link: Clone, Renderer: core::text::Renderer, { layout::sized(limits, width, height, |limits| { @@ -305,13 +430,15 @@ where }) } -impl<'a, Theme, Renderer> FromIterator<Span<'a, Renderer::Font>> - for Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> + FromIterator<Span<'a, Link, Renderer::Font>> + for Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { - fn from_iter<T: IntoIterator<Item = Span<'a, Renderer::Font>>>( + fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>( spans: T, ) -> Self { Self { @@ -321,14 +448,17 @@ where } } -impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> +impl<'a, Message, Link, Theme, Renderer> + From<Rich<'a, Message, Link, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where + Message: 'a, + Link: Clone + 'static, Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { fn from( - text: Rich<'a, Theme, Renderer>, + text: Rich<'a, Message, Link, Theme, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(text) } From 54500e61ed0ff2309f06dd5b441f9f5b627e05c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 13:01:27 +0200 Subject: [PATCH 133/657] Simplify font attributes in `Paragraph::with_spans` --- graphics/src/text/paragraph.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index b69110b2..da703ceb 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -123,15 +123,7 @@ impl core::text::Paragraph for Paragraph { buffer.set_rich_text( font_system.raw(), text.content.iter().enumerate().map(|(i, span)| { - let attrs = if let Some(font) = span.font { - cosmic_text::Attrs::new() - .family(text::to_family(font.family)) - .weight(text::to_weight(font.weight)) - .stretch(text::to_stretch(font.stretch)) - .style(text::to_style(font.style)) - } else { - text::to_attributes(text.font) - }; + let attrs = text::to_attributes(span.font.unwrap_or(text.font)); let attrs = match (span.size, span.line_height) { (None, None) => attrs, From 7072c696a0a6d2a20e4cf5b44952360e15855d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 13:12:38 +0200 Subject: [PATCH 134/657] Rename `on_link_click` to `on_link` --- widget/src/markdown.rs | 12 ++++++------ widget/src/text/rich.rs | 15 ++++++--------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index dc207a34..ae4020bc 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -278,7 +278,7 @@ pub fn parse( /// You can obtain the items with [`parse`]. pub fn view<'a, Message, Renderer>( items: impl IntoIterator<Item = &'a Item>, - on_link_click: impl Fn(String) -> Message + Copy + 'a, + on_link: impl Fn(String) -> Message + Copy + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, @@ -286,16 +286,16 @@ where { let blocks = items.into_iter().enumerate().map(|(i, item)| match item { Item::Heading(heading) => { - container(rich_text(heading).on_link_click(on_link_click)) + container(rich_text(heading).on_link(on_link)) .padding(padding::top(if i > 0 { 8 } else { 0 })) .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).on_link_click(on_link_click).into() + rich_text(paragraph).on_link(on_link).into() } Item::List { start: None, items } => { column(items.iter().map(|items| { - row!["•", view(items, on_link_click)].spacing(10).into() + row!["•", view(items, on_link)].spacing(10).into() })) .spacing(10) .into() @@ -304,7 +304,7 @@ where start: Some(start), items, } => column(items.iter().enumerate().map(|(i, items)| { - row![text!("{}.", i as u64 + *start), view(items, on_link_click)] + row![text!("{}.", i as u64 + *start), view(items, on_link)] .spacing(10) .into() })) @@ -314,7 +314,7 @@ where rich_text(code) .font(Font::MONOSPACE) .size(12) - .on_link_click(on_link_click), + .on_link(on_link), ) .width(Length::Fill) .padding(10) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index a44775c6..8e5e8be1 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -37,7 +37,7 @@ pub struct Rich< align_x: alignment::Horizontal, align_y: alignment::Vertical, class: Theme::Class<'a>, - on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>, + on_link: Option<Box<dyn Fn(Link) -> Message + 'a>>, } impl<'a, Message, Link, Theme, Renderer> @@ -59,7 +59,7 @@ where align_x: alignment::Horizontal::Left, align_y: alignment::Vertical::Top, class: Theme::default(), - on_link_click: None, + on_link: None, } } @@ -156,11 +156,8 @@ where } /// Sets the message handler for link clicks on the [`Rich`] text. - pub fn on_link_click( - mut self, - on_link_click: impl Fn(Link) -> Message + 'a, - ) -> Self { - self.on_link_click = Some(Box::new(on_link_click)); + pub fn on_link(mut self, on_link: impl Fn(Link) -> Message + 'a) -> Self { + self.on_link = Some(Box::new(on_link)); self } @@ -285,7 +282,7 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - let Some(on_link_click) = self.on_link_click.as_ref() else { + let Some(on_link_click) = self.on_link.as_ref() else { return event::Status::Ignored; }; @@ -342,7 +339,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if self.on_link_click.is_none() { + if self.on_link.is_none() { return mouse::Interaction::None; } From 78c4f7e64646f84ae58bf78c36dabe02b35c4e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 13:59:46 +0200 Subject: [PATCH 135/657] Use latest `spans` to retreive `Link` in `rich_text` --- widget/src/text/rich.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 8e5e8be1..625ea089 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -312,7 +312,7 @@ where { match state.paragraph.hit_span(position) { Some(span) if span == span_pressed => { - if let Some(link) = state + if let Some(link) = self .spans .get(span) .and_then(|span| span.link.clone()) @@ -351,7 +351,7 @@ where if let Some(span) = state .paragraph .hit_span(position) - .and_then(|span| state.spans.get(span)) + .and_then(|span| self.spans.get(span)) { if span.link.is_some() { return mouse::Interaction::Pointer; From a2943798a3cf79e15344063fbf4ea8c84d261d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 14:10:39 +0200 Subject: [PATCH 136/657] Use `open::that_in_background` in `markdown` example --- examples/markdown/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index bb6eb57b..ee5b5aab 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -52,7 +52,7 @@ impl Markdown { } } Message::LinkClicked(link) => { - let _ = open::that(link); + let _ = open::that_in_background(link); } } } From f830454ffad1cf60f1d6e56fe95514af96848a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 18:16:32 +0200 Subject: [PATCH 137/657] Use `url` for `markdown` links --- Cargo.toml | 1 + examples/markdown/src/main.rs | 4 ++-- widget/Cargo.toml | 5 ++++- widget/src/markdown.rs | 23 +++++++++++++++++------ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d301b36d..7eea63dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,6 +172,7 @@ tiny-skia = "0.11" tokio = "1.0" tracing = "0.1" unicode-segmentation = "1.0" +url = "2.5" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "=0.3.67" diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index ee5b5aab..47fcfc72 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -16,7 +16,7 @@ struct Markdown { #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), - LinkClicked(String), + LinkClicked(markdown::Url), } impl Markdown { @@ -52,7 +52,7 @@ impl Markdown { } } Message::LinkClicked(link) => { - let _ = open::that_in_background(link); + let _ = open::that_in_background(link.to_string()); } } } diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 2f483b79..98a81145 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -24,7 +24,7 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "dep:qrcode"] wgpu = ["iced_renderer/wgpu"] -markdown = ["dep:pulldown-cmark"] +markdown = ["dep:pulldown-cmark", "dep:url"] highlighter = ["dep:iced_highlighter"] advanced = [] @@ -49,3 +49,6 @@ pulldown-cmark.optional = true iced_highlighter.workspace = true iced_highlighter.optional = true + +url.workspace = true +url.optional = true diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index ae4020bc..1df35036 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -10,17 +10,19 @@ use crate::core::theme::{self, Theme}; use crate::core::{self, Element, Length}; use crate::{column, container, rich_text, row, span, text}; +pub use url::Url; + /// A Markdown item. #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(Vec<text::Span<'static, String>>), + Heading(Vec<text::Span<'static, Url>>), /// A paragraph. - Paragraph(Vec<text::Span<'static, String>>), + Paragraph(Vec<text::Span<'static, Url>>), /// A code block. /// /// You can enable the `highlighter` feature for syntax highligting. - CodeBlock(Vec<text::Span<'static, String>>), + CodeBlock(Vec<text::Span<'static, Url>>), /// A list. List { /// The first number of the list, if it is ordered. @@ -96,7 +98,16 @@ pub fn parse( pulldown_cmark::Tag::Link { dest_url, .. } if !metadata && !table => { - link = Some(dest_url); + match Url::parse(&dest_url) { + Ok(url) + if url.scheme() == "http" + || url.scheme() == "https" => + { + link = Some(url); + } + _ => {} + } + None } pulldown_cmark::Tag::List(first_item) if !metadata && !table => { @@ -248,7 +259,7 @@ pub fn parse( }; let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.to_string()) + span.color(palette.primary).link(link.clone()) } else { span }; @@ -278,7 +289,7 @@ pub fn parse( /// You can obtain the items with [`parse`]. pub fn view<'a, Message, Renderer>( items: impl IntoIterator<Item = &'a Item>, - on_link: impl Fn(String) -> Message + Copy + 'a, + on_link: impl Fn(Url) -> Message + Copy + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, From 65b525af7ff2823cfe635c4b26d33aad9068e392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 20:00:02 +0200 Subject: [PATCH 138/657] Introduce `markdown::Settings` --- core/src/pixels.rs | 21 +++++ examples/markdown/src/main.rs | 6 +- widget/src/markdown.rs | 157 +++++++++++++++++++++++++--------- widget/src/text/rich.rs | 9 ++ 4 files changed, 151 insertions(+), 42 deletions(-) diff --git a/core/src/pixels.rs b/core/src/pixels.rs index f5550a10..a1ea0f15 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -9,6 +9,11 @@ #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] pub struct Pixels(pub f32); +impl Pixels { + /// Zero pixels. + pub const ZERO: Self = Self(0.0); +} + impl From<f32> for Pixels { fn from(amount: f32) -> Self { Self(amount) @@ -58,3 +63,19 @@ impl std::ops::Mul<f32> for Pixels { Pixels(self.0 * rhs) } } + +impl std::ops::Div for Pixels { + type Output = Pixels; + + fn div(self, rhs: Self) -> Self { + Pixels(self.0 / rhs.0) + } +} + +impl std::ops::Div<f32> for Pixels { + type Output = Pixels; + + fn div(self, rhs: f32) -> Self { + Pixels(self.0 / rhs) + } +} diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 47fcfc72..44bf57c5 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -64,7 +64,11 @@ impl Markdown { .padding(10) .font(Font::MONOSPACE); - let preview = markdown(&self.items, Message::LinkClicked); + let preview = markdown( + &self.items, + markdown::Settings::default(), + Message::LinkClicked, + ); row![editor, scrollable(preview).spacing(10).height(Fill)] .spacing(10) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 1df35036..e84ff8d6 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -7,16 +7,17 @@ use crate::core::font::{self, Font}; use crate::core::padding; use crate::core::theme::{self, Theme}; -use crate::core::{self, Element, Length}; +use crate::core::{self, Element, Length, Pixels}; use crate::{column, container, rich_text, row, span, text}; +pub use pulldown_cmark::HeadingLevel; pub use url::Url; /// A Markdown item. #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(Vec<text::Span<'static, Url>>), + Heading(pulldown_cmark::HeadingLevel, Vec<text::Span<'static, Url>>), /// A paragraph. Paragraph(Vec<text::Span<'static, Url>>), /// A code block. @@ -43,7 +44,6 @@ pub fn parse( } let mut spans = Vec::new(); - let mut heading = None; let mut strong = false; let mut emphasis = false; let mut metadata = false; @@ -81,12 +81,6 @@ pub fn parse( #[allow(clippy::drain_collect)] parser.filter_map(move |event| match event { pulldown_cmark::Event::Start(tag) => match tag { - pulldown_cmark::Tag::Heading { level, .. } - if !metadata && !table => - { - heading = Some(level); - None - } pulldown_cmark::Tag::Strong if !metadata && !table => { strong = true; None @@ -119,7 +113,11 @@ pub fn parse( None } pulldown_cmark::Tag::Item => { - lists.last_mut().expect("List").items.push(Vec::new()); + lists + .last_mut() + .expect("list context") + .items + .push(Vec::new()); None } pulldown_cmark::Tag::CodeBlock( @@ -150,9 +148,11 @@ pub fn parse( _ => None, }, pulldown_cmark::Event::End(tag) => match tag { - pulldown_cmark::TagEnd::Heading(_) if !metadata && !table => { - heading = None; - produce(&mut lists, Item::Heading(spans.drain(..).collect())) + pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { + produce( + &mut lists, + Item::Heading(level, spans.drain(..).collect()), + ) } pulldown_cmark::TagEnd::Emphasis if !metadata && !table => { emphasis = false; @@ -180,7 +180,7 @@ pub fn parse( } } pulldown_cmark::TagEnd::List(_) if !metadata && !table => { - let list = lists.pop().expect("List"); + let list = lists.pop().expect("list context"); produce( &mut lists, @@ -228,18 +228,6 @@ pub fn parse( let span = span(text.into_string()); - let span = match heading { - None => span, - Some(heading) => span.size(match heading { - pulldown_cmark::HeadingLevel::H1 => 32, - pulldown_cmark::HeadingLevel::H2 => 28, - pulldown_cmark::HeadingLevel::H3 => 24, - pulldown_cmark::HeadingLevel::H4 => 20, - pulldown_cmark::HeadingLevel::H5 => 16, - pulldown_cmark::HeadingLevel::H6 => 16, - }), - }; - let span = if strong || emphasis { span.font(Font { weight: if strong { @@ -269,7 +257,15 @@ pub fn parse( None } pulldown_cmark::Event::Code(code) if !metadata && !table => { - spans.push(span(code.into_string()).font(Font::MONOSPACE)); + let span = span(code.into_string()).font(Font::MONOSPACE); + + let span = if let Some(link) = link.as_ref() { + span.color(palette.primary).link(link.clone()) + } else { + span + }; + + spans.push(span); None } pulldown_cmark::Event::SoftBreak if !metadata && !table => { @@ -284,54 +280,133 @@ pub fn parse( }) } +/// Configuration controlling Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy)] +pub struct Settings { + /// The base text size. + pub text_size: Pixels, + /// The text size of level 1 heading. + pub h1_size: Pixels, + /// The text size of level 2 heading. + pub h2_size: Pixels, + /// The text size of level 3 heading. + pub h3_size: Pixels, + /// The text size of level 4 heading. + pub h4_size: Pixels, + /// The text size of level 5 heading. + pub h5_size: Pixels, + /// The text size of level 6 heading. + pub h6_size: Pixels, + /// The text size used in code blocks. + pub code_size: Pixels, +} + +impl Settings { + /// Creates new [`Settings`] with the given base text size in [`Pixels`]. + /// + /// Heading levels will be adjusted automatically. Specifically, + /// the first level will be twice the base size, and then every level + /// after that will be 25% smaller. + pub fn with_text_size(text_size: impl Into<Pixels>) -> Self { + let text_size = text_size.into(); + + Self { + text_size, + h1_size: text_size * 2.0, + h2_size: text_size * 1.75, + h3_size: text_size * 1.5, + h4_size: text_size * 1.25, + h5_size: text_size, + h6_size: text_size, + code_size: text_size * 0.75, + } + } +} + +impl Default for Settings { + fn default() -> Self { + Self::with_text_size(16) + } +} + /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. pub fn view<'a, Message, Renderer>( items: impl IntoIterator<Item = &'a Item>, + settings: Settings, on_link: impl Fn(Url) -> Message + Copy + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, Renderer: core::text::Renderer<Font = Font> + 'a, { + let Settings { + text_size, + h1_size, + h2_size, + h3_size, + h4_size, + h5_size, + h6_size, + code_size, + } = settings; + + let spacing = text_size * 0.625; + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { - Item::Heading(heading) => { - container(rich_text(heading).on_link(on_link)) - .padding(padding::top(if i > 0 { 8 } else { 0 })) - .into() + Item::Heading(level, heading) => { + container(rich_text(heading).on_link(on_link).size(match level { + pulldown_cmark::HeadingLevel::H1 => h1_size, + pulldown_cmark::HeadingLevel::H2 => h2_size, + pulldown_cmark::HeadingLevel::H3 => h3_size, + pulldown_cmark::HeadingLevel::H4 => h4_size, + pulldown_cmark::HeadingLevel::H5 => h5_size, + pulldown_cmark::HeadingLevel::H6 => h6_size, + })) + .padding(padding::top(if i > 0 { + text_size / 2.0 + } else { + Pixels::ZERO + })) + .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).on_link(on_link).into() + rich_text(paragraph).on_link(on_link).size(text_size).into() } Item::List { start: None, items } => { column(items.iter().map(|items| { - row!["•", view(items, on_link)].spacing(10).into() + row![text("•").size(text_size), view(items, settings, on_link)] + .spacing(spacing) + .into() })) - .spacing(10) + .spacing(spacing) .into() } Item::List { start: Some(start), items, } => column(items.iter().enumerate().map(|(i, items)| { - row![text!("{}.", i as u64 + *start), view(items, on_link)] - .spacing(10) - .into() + row![ + text!("{}.", i as u64 + *start).size(text_size), + view(items, settings, on_link) + ] + .spacing(spacing) + .into() })) - .spacing(10) + .spacing(spacing) .into(), Item::CodeBlock(code) => container( rich_text(code) .font(Font::MONOSPACE) - .size(12) + .size(code_size) .on_link(on_link), ) .width(Length::Fill) - .padding(10) + .padding(spacing.0) .style(container::rounded_box) .into(), }); - Element::new(column(blocks).width(Length::Fill).spacing(16)) + Element::new(column(blocks).width(Length::Fill).spacing(text_size)) } diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 625ea089..05ad6576 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -161,6 +161,15 @@ where self } + /// Sets the message handler for link clicks on the [`Rich`] text. + pub fn on_link_maybe( + mut self, + on_link: Option<impl Fn(Link) -> Message + 'a>, + ) -> Self { + self.on_link = on_link.map(|on_link| Box::new(on_link) as _); + self + } + /// Sets the default style class of the [`Rich`] text. #[cfg(feature = "advanced")] #[must_use] From dcdf1307006883f50083c186ca7b8656bfa60873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 21 Jul 2024 20:07:58 +0200 Subject: [PATCH 139/657] Use horizontal `scrollable` for code blocks in `markdown` widget --- widget/src/markdown.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index e84ff8d6..f2b69244 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -8,7 +8,7 @@ use crate::core::font::{self, Font}; use crate::core::padding; use crate::core::theme::{self, Theme}; use crate::core::{self, Element, Length, Pixels}; -use crate::{column, container, rich_text, row, span, text}; +use crate::{column, container, rich_text, row, scrollable, span, text}; pub use pulldown_cmark::HeadingLevel; pub use url::Url; @@ -397,13 +397,23 @@ where .spacing(spacing) .into(), Item::CodeBlock(code) => container( - rich_text(code) - .font(Font::MONOSPACE) - .size(code_size) - .on_link(on_link), + scrollable( + container( + rich_text(code) + .font(Font::MONOSPACE) + .size(code_size) + .on_link(on_link), + ) + .padding(spacing.0 / 2.0), + ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::default() + .width(spacing.0 / 2.0) + .scroller_width(spacing.0 / 2.0), + )), ) .width(Length::Fill) - .padding(spacing.0) + .padding(spacing.0 / 2.0) .style(container::rounded_box) .into(), }); From c47844ca2b0dd3efbcb0a850878867bbb2a59a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 23 Jul 2024 18:15:15 +0200 Subject: [PATCH 140/657] Fix `span` widget helper missing `Link` generic --- widget/src/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 5b1cb5bc..dcc668ca 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -697,9 +697,9 @@ where /// Creates a new [`Span`] of text with the provided content. /// /// [`Span`]: text::Span -pub fn span<'a, Font>( +pub fn span<'a, Link, Font>( text: impl text::IntoFragment<'a>, -) -> text::Span<'a, Font> { +) -> text::Span<'a, Link, Font> { text::Span::new(text) } From a8c772eb8ab8854ec87b963d855966ee0ad5ea51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 23 Jul 2024 18:18:11 +0200 Subject: [PATCH 141/657] Fix mssing `Link` generic in `From` impl for `Span` --- core/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/text.rs b/core/src/text.rs index c22734f8..aa24d85f 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -313,7 +313,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { } } -impl<'a, Font> From<&'a str> for Span<'a, Font> { +impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> { fn from(value: &'a str) -> Self { Span::new(value) } From faa5d0c58d6174091253fd55820d286c23b80ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 24 Jul 2024 10:12:33 +0200 Subject: [PATCH 142/657] Unify `Link` and `Message` generics in `text::Rich` --- examples/markdown/src/main.rs | 7 +--- widget/src/helpers.rs | 6 +-- widget/src/markdown.rs | 19 ++++------ widget/src/text/rich.rs | 69 +++++++++-------------------------- 4 files changed, 29 insertions(+), 72 deletions(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 44bf57c5..163dab66 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -64,11 +64,8 @@ impl Markdown { .padding(10) .font(Font::MONOSPACE); - let preview = markdown( - &self.items, - markdown::Settings::default(), - Message::LinkClicked, - ); + let preview = markdown(&self.items, markdown::Settings::default()) + .map(Message::LinkClicked); row![editor, scrollable(preview).spacing(10).height(Fill)] .spacing(10) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index dcc668ca..0eb5f974 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -683,11 +683,11 @@ where /// Creates a new [`Rich`] text widget with the provided spans. /// /// [`Rich`]: text::Rich -pub fn rich_text<'a, Message, Link, Theme, Renderer>( +pub fn rich_text<'a, Link, Theme, Renderer>( spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>, -) -> text::Rich<'a, Message, Link, Theme, Renderer> +) -> text::Rich<'a, Link, Theme, Renderer> where - Link: Clone, + Link: Clone + 'static, Theme: text::Catalog + 'a, Renderer: core::text::Renderer, { diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index f2b69244..0e47d35c 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -332,13 +332,11 @@ impl Default for Settings { /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. -pub fn view<'a, Message, Renderer>( +pub fn view<'a, Renderer>( items: impl IntoIterator<Item = &'a Item>, settings: Settings, - on_link: impl Fn(Url) -> Message + Copy + 'a, -) -> Element<'a, Message, Theme, Renderer> +) -> Element<'a, Url, Theme, Renderer> where - Message: 'a, Renderer: core::text::Renderer<Font = Font> + 'a, { let Settings { @@ -356,7 +354,7 @@ where let blocks = items.into_iter().enumerate().map(|(i, item)| match item { Item::Heading(level, heading) => { - container(rich_text(heading).on_link(on_link).size(match level { + container(rich_text(heading).size(match level { pulldown_cmark::HeadingLevel::H1 => h1_size, pulldown_cmark::HeadingLevel::H2 => h2_size, pulldown_cmark::HeadingLevel::H3 => h3_size, @@ -372,11 +370,11 @@ where .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).on_link(on_link).size(text_size).into() + rich_text(paragraph).size(text_size).into() } Item::List { start: None, items } => { column(items.iter().map(|items| { - row![text("•").size(text_size), view(items, settings, on_link)] + row![text("•").size(text_size), view(items, settings)] .spacing(spacing) .into() })) @@ -389,7 +387,7 @@ where } => column(items.iter().enumerate().map(|(i, items)| { row![ text!("{}.", i as u64 + *start).size(text_size), - view(items, settings, on_link) + view(items, settings) ] .spacing(spacing) .into() @@ -399,10 +397,7 @@ where Item::CodeBlock(code) => container( scrollable( container( - rich_text(code) - .font(Font::MONOSPACE) - .size(code_size) - .on_link(on_link), + rich_text(code).font(Font::MONOSPACE).size(code_size), ) .padding(spacing.0 / 2.0), ) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 05ad6576..9c0e2fac 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -17,13 +17,8 @@ use std::borrow::Cow; /// A bunch of [`Rich`] text. #[allow(missing_debug_implementations)] -pub struct Rich< - 'a, - Message, - Link = (), - Theme = crate::Theme, - Renderer = crate::Renderer, -> where +pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer> +where Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, @@ -37,11 +32,9 @@ pub struct Rich< align_x: alignment::Horizontal, align_y: alignment::Vertical, class: Theme::Class<'a>, - on_link: Option<Box<dyn Fn(Link) -> Message + 'a>>, } -impl<'a, Message, Link, Theme, Renderer> - Rich<'a, Message, Link, Theme, Renderer> +impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer> where Link: Clone + 'static, Theme: Catalog, @@ -59,7 +52,6 @@ where align_x: alignment::Horizontal::Left, align_y: alignment::Vertical::Top, class: Theme::default(), - on_link: None, } } @@ -155,21 +147,6 @@ where self.style(move |_theme| Style { color }) } - /// Sets the message handler for link clicks on the [`Rich`] text. - pub fn on_link(mut self, on_link: impl Fn(Link) -> Message + 'a) -> Self { - self.on_link = Some(Box::new(on_link)); - self - } - - /// Sets the message handler for link clicks on the [`Rich`] text. - pub fn on_link_maybe( - mut self, - on_link: Option<impl Fn(Link) -> Message + 'a>, - ) -> Self { - self.on_link = on_link.map(|on_link| Box::new(on_link) as _); - self - } - /// Sets the default style class of the [`Rich`] text. #[cfg(feature = "advanced")] #[must_use] @@ -188,10 +165,9 @@ where } } -impl<'a, Message, Link, Theme, Renderer> Default - for Rich<'a, Message, Link, Theme, Renderer> +impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer> where - Link: Clone + 'static, + Link: Clone + 'a, Theme: Catalog, Renderer: core::text::Renderer, { @@ -206,8 +182,8 @@ struct State<Link, P: Paragraph> { paragraph: P, } -impl<'a, Message, Link, Theme, Renderer> Widget<Message, Theme, Renderer> - for Rich<'a, Message, Link, Theme, Renderer> +impl<'a, Link, Theme, Renderer> Widget<Link, Theme, Renderer> + for Rich<'a, Link, Theme, Renderer> where Link: Clone + 'static, Theme: Catalog, @@ -288,13 +264,9 @@ where cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, + shell: &mut Shell<'_, Link>, _viewport: &Rectangle, ) -> event::Status { - let Some(on_link_click) = self.on_link.as_ref() else { - return event::Status::Ignored; - }; - match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { if let Some(position) = cursor.position_in(layout.bounds()) { @@ -326,7 +298,7 @@ where .get(span) .and_then(|span| span.link.clone()) { - shell.publish(on_link_click(link)); + shell.publish(link); } } _ => {} @@ -348,10 +320,6 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if self.on_link.is_none() { - return mouse::Interaction::None; - } - if let Some(position) = cursor.position_in(layout.bounds()) { let state = tree .state @@ -436,11 +404,10 @@ where }) } -impl<'a, Message, Link, Theme, Renderer> - FromIterator<Span<'a, Link, Renderer::Font>> - for Rich<'a, Message, Link, Theme, Renderer> +impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>> + for Rich<'a, Link, Theme, Renderer> where - Link: Clone + 'static, + Link: Clone + 'a, Theme: Catalog, Renderer: core::text::Renderer, { @@ -454,18 +421,16 @@ where } } -impl<'a, Message, Link, Theme, Renderer> - From<Rich<'a, Message, Link, Theme, Renderer>> - for Element<'a, Message, Theme, Renderer> +impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>> + for Element<'a, Link, Theme, Renderer> where - Message: 'a, - Link: Clone + 'static, + Link: Clone + 'a, Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { fn from( - text: Rich<'a, Message, Link, Theme, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { + text: Rich<'a, Link, Theme, Renderer>, + ) -> Element<'a, Link, Theme, Renderer> { Element::new(text) } } From 2eea9b81e49121bdfe4df6f558f1f7f9222d082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 24 Jul 2024 10:34:24 +0200 Subject: [PATCH 143/657] Exit runtime with `control_sender` instead of `break` --- winit/src/program.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index e7d3294d..0e1849eb 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -661,7 +661,7 @@ async fn run_instance<P, C>( debug.startup_finished(); - 'main: while let Some(event) = event_receiver.next().await { + while let Some(event) = event_receiver.next().await { match event { Event::WindowCreated { id, @@ -929,7 +929,11 @@ async fn run_instance<P, C>( && window_id != boot_window && window_manager.is_empty() { - break 'main; + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + + continue; } let Some((id, window)) = From 884c66ca15eb20a7c591c91e6a68aadd4b416ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 24 Jul 2024 10:38:51 +0200 Subject: [PATCH 144/657] Depend on `wasm-bindgen-futures` only for Wasm Fixes #2518 --- winit/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 68368aa1..f5a47952 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -33,7 +33,6 @@ log.workspace = true rustc-hash.workspace = true thiserror.workspace = true tracing.workspace = true -wasm-bindgen-futures.workspace = true window_clipboard.workspace = true winit.workspace = true @@ -46,4 +45,4 @@ winapi.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys.workspace = true web-sys.features = ["Document", "Window"] - +wasm-bindgen-futures.workspace = true From a5b1a1df548a9bfeefc3e422defe6d67cf61c170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 24 Jul 2024 12:18:53 +0200 Subject: [PATCH 145/657] Fix macOS race condition when closing window --- winit/src/program.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 0e1849eb..872c8a98 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -320,7 +320,6 @@ where .send(Boot { compositor, clipboard, - window: window.id(), }) .ok() .expect("Send boot event"); @@ -601,9 +600,9 @@ where struct Boot<C> { compositor: C, clipboard: Clipboard, - window: winit::window::WindowId, } +#[derive(Debug)] enum Event<Message: 'static> { WindowCreated { id: window::Id, @@ -615,6 +614,7 @@ enum Event<Message: 'static> { EventLoopAwakened(winit::event::Event<Message>), } +#[derive(Debug)] enum Control { ChangeFlow(winit::event_loop::ControlFlow), Exit, @@ -647,10 +647,10 @@ async fn run_instance<P, C>( let Boot { mut compositor, mut clipboard, - window: boot_window, } = boot.try_recv().ok().flatten().expect("Receive boot"); let mut window_manager = WindowManager::new(); + let mut boot_window_closed = false; let mut events = Vec::new(); let mut messages = Vec::new(); @@ -926,14 +926,16 @@ async fn run_instance<P, C>( window_event, winit::event::WindowEvent::Destroyed ) - && window_id != boot_window - && window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); + if boot_window_closed && window_manager.is_empty() { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); - continue; + continue; + } + + boot_window_closed = true; } let Some((id, window)) = From e9e06c8fe2ef291b04f6059a841ceb999455bea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 24 Jul 2024 14:52:01 +0200 Subject: [PATCH 146/657] Add `placeholder` support to `text_editor` widget --- core/src/renderer/null.rs | 4 +++ core/src/text/editor.rs | 3 ++ examples/markdown/src/main.rs | 1 + graphics/src/text/editor.rs | 7 ++++ widget/src/text_editor.rs | 62 ++++++++++++++++++++++++++++++----- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 7aa3aafb..d8d3c50a 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -118,6 +118,10 @@ impl text::Editor for () { fn with_text(_text: &str) -> Self {} + fn is_empty(&self) -> bool { + true + } + fn cursor(&self) -> text::editor::Cursor { text::editor::Cursor::Caret(Point::ORIGIN) } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index aea00921..135707d1 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -13,6 +13,9 @@ pub trait Editor: Sized + Default { /// Creates a new [`Editor`] laid out with the given text. fn with_text(text: &str) -> Self; + /// Returns true if the [`Editor`] has no contents. + fn is_empty(&self) -> bool; + /// Returns the current [`Cursor`] of the [`Editor`]. fn cursor(&self) -> Cursor; diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 163dab66..173cb389 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -59,6 +59,7 @@ impl Markdown { fn view(&self) -> Element<Message> { let editor = text_editor(&self.content) + .placeholder("Type your Markdown here...") .on_action(Message::Edit) .height(Fill) .padding(10) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 3e6ef70c..80733bbb 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -82,6 +82,13 @@ impl editor::Editor for Editor { }))) } + fn is_empty(&self) -> bool { + let buffer = self.buffer(); + + buffer.lines.is_empty() + || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) + } + fn line(&self, index: usize) -> Option<&str> { self.buffer() .lines diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index e494a3b0..0bfc8500 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,4 +1,5 @@ //! Display a multi-line text input for text editing. +use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -8,7 +9,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::{self, LineHeight}; +use crate::core::text::{self, LineHeight, Text}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ @@ -37,6 +38,7 @@ pub struct TextEditor< Renderer: text::Renderer, { content: &'a Content<Renderer>, + placeholder: Option<text::Fragment<'a>>, font: Option<Renderer::Font>, text_size: Option<Pixels>, line_height: LineHeight, @@ -62,6 +64,7 @@ where pub fn new(content: &'a Content<Renderer>) -> Self { Self { content, + placeholder: None, font: None, text_size: None, line_height: LineHeight::default(), @@ -85,6 +88,15 @@ where Theme: Catalog, Renderer: text::Renderer, { + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder( + mut self, + placeholder: impl text::IntoFragment<'a>, + ) -> Self { + self.placeholder = Some(placeholder.into_fragment()); + self + } + /// Sets the height of the [`TextEditor`]. pub fn height(mut self, height: impl Into<Length>) -> Self { self.height = height.into(); @@ -144,6 +156,7 @@ where ) -> TextEditor<'a, H, Message, Theme, Renderer> { TextEditor { content: self.content, + placeholder: self.placeholder, font: self.font, text_size: self.text_size, line_height: self.line_height, @@ -546,8 +559,10 @@ where let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_ref::<State<Highlighter>>(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); + internal.editor.highlight( - self.font.unwrap_or_else(|| renderer.default_font()), + font, state.highlighter.borrow_mut().deref_mut(), |highlight| (self.highlighter_format)(highlight, theme), ); @@ -576,13 +591,42 @@ where style.background, ); - renderer.fill_editor( - &internal.editor, - bounds.position() - + Vector::new(self.padding.left, self.padding.top), - defaults.text_color, - *viewport, - ); + let position = bounds.position() + + Vector::new(self.padding.left, self.padding.top); + + if internal.editor.is_empty() { + if let Some(placeholder) = self.placeholder.clone() { + renderer.fill_text( + Text { + content: placeholder.into_owned(), + bounds: bounds.size() + - Size::new( + self.padding.right, + self.padding.bottom, + ), + + size: self + .text_size + .unwrap_or_else(|| renderer.default_size()), + line_height: self.line_height, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }, + position, + style.placeholder, + *viewport, + ); + } + } else { + renderer.fill_editor( + &internal.editor, + position, + defaults.text_color, + *viewport, + ); + } let translation = Vector::new( bounds.x + self.padding.left, From f18f08bd617edbf58787ec4f379aa52c0ef292e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 24 Jul 2024 14:57:32 +0200 Subject: [PATCH 147/657] Fix broken doc link in `text_editor` --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0bfc8500..4871a0f5 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -88,7 +88,7 @@ where Theme: Catalog, Renderer: text::Renderer, { - /// Sets the placeholder of the [`PickList`]. + /// Sets the placeholder of the [`TextEditor`]. pub fn placeholder( mut self, placeholder: impl text::IntoFragment<'a>, From 555ee3e9c66010c9a90c3ef55d61fbffd48e669d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 26 Jul 2024 11:01:33 +0200 Subject: [PATCH 148/657] Fix lints for Rust 1.80 --- Cargo.toml | 2 +- core/src/overlay.rs | 2 +- core/src/widget.rs | 6 +++--- futures/src/subscription.rs | 4 ++-- futures/src/subscription/tracker.rs | 6 +++--- widget/src/radio.rs | 2 +- widget/src/slider.rs | 4 ++-- widget/src/vertical_slider.rs | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7eea63dd..aa2d950e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,7 +183,7 @@ window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } [workspace.lints.rust] -rust_2018_idioms = "forbid" +rust_2018_idioms = { level = "forbid", priority = -1 } missing_debug_implementations = "deny" missing_docs = "deny" unsafe_code = "deny" diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 16f867da..3b79970e 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -52,7 +52,7 @@ where /// * the computed [`Layout`] of the [`Overlay`] /// * the current cursor position /// * a mutable `Message` list, allowing the [`Overlay`] to produce - /// new messages based on user interaction. + /// new messages based on user interaction. /// * the `Renderer` /// * a [`Clipboard`], if available /// diff --git a/core/src/widget.rs b/core/src/widget.rs index b17215bb..08cfa55b 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -27,11 +27,11 @@ use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector}; /// widget: /// /// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using -/// [`lyon`]. +/// [`lyon`]. /// - [`custom_widget`], a demonstration of how to build a custom widget that -/// draws a circle. +/// draws a circle. /// - [`geometry`], a custom widget showcasing how to draw geometry with the -/// `Mesh2D` primitive in [`iced_wgpu`]. +/// `Mesh2D` primitive in [`iced_wgpu`]. /// /// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples /// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.12/examples/bezier_tool diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 1a0d454d..d2a0c3f8 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -313,9 +313,9 @@ impl<T> std::fmt::Debug for Subscription<T> { /// The repository has a couple of [examples] that use a custom [`Recipe`]: /// /// - [`download_progress`], a basic application that asynchronously downloads -/// a dummy file of 100 MB and tracks the download progress. +/// a dummy file of 100 MB and tracks the download progress. /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how -/// to listen to time. +/// to listen to time. /// /// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples /// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index f17e3ea3..6daead24 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -42,10 +42,10 @@ impl Tracker { /// method: /// /// - If the provided [`Subscription`] contains a new [`Recipe`] that is - /// currently not being run, it will spawn a new stream and keep it alive. + /// currently not being run, it will spawn a new stream and keep it alive. /// - On the other hand, if a [`Recipe`] is currently in execution and the - /// provided [`Subscription`] does not contain it anymore, then the - /// [`Tracker`] will close and drop the relevant stream. + /// provided [`Subscription`] does not contain it anymore, then the + /// [`Tracker`] will close and drop the relevant stream. /// /// It returns a list of futures that need to be spawned to materialize /// the [`Tracker`] changes. diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 536a7483..1b02f8ca 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -105,7 +105,7 @@ where /// * the label of the [`Radio`] button /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It - /// receives the value of the radio and must produce a `Message`. + /// receives the value of the radio and must produce a `Message`. pub fn new<F, V>( label: impl Into<String>, value: V, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index b9419232..e586684a 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -70,8 +70,8 @@ where /// * an inclusive range of possible values /// * the current value of the [`Slider`] /// * a function that will be called when the [`Slider`] is dragged. - /// It receives the new value of the [`Slider`] and must produce a - /// `Message`. + /// It receives the new value of the [`Slider`] and must produce a + /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 6185295b..f21b996c 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -72,8 +72,8 @@ where /// * an inclusive range of possible values /// * the current value of the [`VerticalSlider`] /// * a function that will be called when the [`VerticalSlider`] is dragged. - /// It receives the new value of the [`VerticalSlider`] and must produce a - /// `Message`. + /// It receives the new value of the [`VerticalSlider`] and must produce a + /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, From 28d8b73846f49d23790e8c91c31fd2168014a6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 26 Jul 2024 10:28:23 +0200 Subject: [PATCH 149/657] Implement custom key binding support for `text_editor` --- widget/src/text_editor.rs | 390 ++++++++++++++++++++++++++------------ 1 file changed, 272 insertions(+), 118 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 4871a0f5..fa7863da 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -13,8 +13,8 @@ use crate::core::text::{self, LineHeight, Text}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ - Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, - Shell, Size, Theme, Vector, + Background, Border, Color, Element, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, SmolStr, Theme, Vector, }; use std::cell::RefCell; @@ -46,6 +46,7 @@ pub struct TextEditor< height: Length, padding: Padding, class: Theme::Class<'a>, + key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -72,6 +73,7 @@ where height: Length::Shrink, padding: Padding::new(5.0), class: Theme::default(), + key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -164,12 +166,24 @@ where height: self.height, padding: self.padding, class: self.class, + key_binding: self.key_binding, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, } } + /// Sets the closure to produce key bindings on key presses. + /// + /// See [`Binding`] for the list of available bindings. + pub fn key_binding( + mut self, + key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a, + ) -> Self { + self.key_binding = Some(Box::new(key_binding)); + self + } + /// Sets the style of the [`TextEditor`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -475,6 +489,7 @@ where layout.bounds(), self.padding, cursor, + self.key_binding.as_deref(), ) else { return event::Status::Ignored; }; @@ -495,6 +510,12 @@ where shell.publish(on_edit(action)); } + Update::Drag(position) => { + shell.publish(on_edit(Action::Drag(position))); + } + Update::Release => { + state.drag_click = None; + } Update::Scroll(lines) => { let bounds = self.content.0.borrow().editor.bounds(); @@ -509,35 +530,102 @@ where lines: lines as i32, })); } - Update::Unfocus => { - state.is_focused = false; - state.drag_click = None; - } - Update::Release => { - state.drag_click = None; - } - Update::Action(action) => { - shell.publish(on_edit(action)); - } - Update::Copy => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - } - } - Update::Cut => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - shell.publish(on_edit(Action::Edit(Edit::Delete))); - } - } - Update::Paste => { - if let Some(contents) = - clipboard.read(clipboard::Kind::Standard) - { - shell.publish(on_edit(Action::Edit(Edit::Paste( - Arc::new(contents), - )))); + Update::Binding(binding) => { + fn apply_binding< + H: text::Highlighter, + R: text::Renderer, + Message, + >( + binding: Binding<Message>, + content: &Content<R>, + state: &mut State<H>, + on_edit: &dyn Fn(Action) -> Message, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + let mut publish = |action| shell.publish(on_edit(action)); + + match binding { + Binding::Unfocus => { + state.is_focused = false; + state.drag_click = None; + } + Binding::Copy => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + } + } + Binding::Cut => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + + publish(Action::Edit(Edit::Delete)); + } + } + Binding::Paste => { + if let Some(contents) = + clipboard.read(clipboard::Kind::Standard) + { + publish(Action::Edit(Edit::Paste(Arc::new( + contents, + )))); + } + } + Binding::Move(motion) => { + publish(Action::Move(motion)); + } + Binding::Select(motion) => { + publish(Action::Select(motion)); + } + Binding::SelectWord => { + publish(Action::SelectWord); + } + Binding::SelectLine => { + publish(Action::SelectLine); + } + Binding::SelectAll => { + publish(Action::SelectAll); + } + Binding::Insert(c) => { + publish(Action::Edit(Edit::Insert(c))); + } + Binding::Enter => { + publish(Action::Edit(Edit::Enter)); + } + Binding::Backspace => { + publish(Action::Edit(Edit::Backspace)); + } + Binding::Delete => { + publish(Action::Edit(Edit::Delete)); + } + Binding::Sequence(sequence) => { + for binding in sequence { + apply_binding( + binding, content, state, on_edit, + clipboard, shell, + ); + } + } + Binding::Custom(message) => { + shell.publish(message); + } + } } + + apply_binding( + binding, + self.content, + state, + on_edit, + clipboard, + shell, + ); } } @@ -731,27 +819,144 @@ where } } -enum Update { - Click(mouse::Click), - Scroll(f32), +/// A binding to an action in the [`TextEditor`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Binding<Message> { + /// Unfocus the [`TextEditor`]. Unfocus, - Release, - Action(Action), + /// Copy the selection of the [`TextEditor`]. Copy, + /// Cut the selection of the [`TextEditor`]. Cut, + /// Paste the clipboard contents in the [`TextEditor`]. Paste, + /// Apply a [`Motion`]. + Move(Motion), + /// Select text with a given [`Motion`]. + Select(Motion), + /// Select the word at the current cursor. + SelectWord, + /// Select the line at the current cursor. + SelectLine, + /// Select the entire buffer. + SelectAll, + /// Insert the given character. + Insert(char), + /// Break the current line. + Enter, + /// Delete the previous character. + Backspace, + /// Delete the next character. + Delete, + /// A sequence of bindings to execute. + Sequence(Vec<Self>), + /// Produce the given message. + Custom(Message), } -impl Update { +/// A key press. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyPress { + /// The key pressed. + pub key: keyboard::Key, + /// The state of the keyboard modifiers. + pub modifiers: keyboard::Modifiers, + /// The text produced by the key press. + pub text: Option<SmolStr>, + /// The current [`Status`] of the [`TextEditor`]. + pub status: Status, +} + +impl<Message> Binding<Message> { + /// Returns the default [`Binding`] for the given key press. + pub fn from_key_press(event: KeyPress) -> Option<Self> { + let KeyPress { + key, + modifiers, + text, + status, + } = event; + + if status != Status::Focused { + return None; + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter), + keyboard::Key::Named(key::Named::Backspace) => { + Some(Self::Backspace) + } + keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete), + keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), + keyboard::Key::Character("c") if modifiers.command() => { + Some(Self::Copy) + } + keyboard::Key::Character("x") if modifiers.command() => { + Some(Self::Cut) + } + keyboard::Key::Character("v") + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } + keyboard::Key::Character("a") if modifiers.command() => { + Some(Self::SelectAll) + } + _ => { + if let Some(text) = text { + let c = text.chars().find(|c| !c.is_control())?; + + Some(Self::Insert(c)) + } else if let keyboard::Key::Named(named_key) = key.as_ref() { + let motion = motion(named_key)?; + + let motion = if modifiers.macos_command() { + match motion { + Motion::Left => Motion::Home, + Motion::Right => Motion::End, + _ => motion, + } + } else { + motion + }; + + let motion = if modifiers.jump() { + motion.widen() + } else { + motion + }; + + Some(if modifiers.shift() { + Self::Select(motion) + } else { + Self::Move(motion) + }) + } else { + None + } + } + } + } +} + +enum Update<Message> { + Click(mouse::Click), + Drag(Point), + Release, + Scroll(f32), + Binding(Binding<Message>), +} + +impl<Message> Update<Message> { fn from_event<H: Highlighter>( event: Event, state: &State<H>, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, + key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>, ) -> Option<Self> { - let action = |action| Some(Update::Action(action)); - let edit = |edit| action(Action::Edit(edit)); + let binding = |binding| Some(Update::Binding(binding)); match event { Event::Mouse(event) => match event { @@ -767,7 +972,7 @@ impl Update { Some(Update::Click(click)) } else if state.is_focused { - Some(Update::Unfocus) + binding(Binding::Unfocus) } else { None } @@ -780,7 +985,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - action(Action::Drag(cursor_position)) + Some(Update::Drag(cursor_position)) } _ => None, }, @@ -800,86 +1005,35 @@ impl Update { } _ => None, }, - Event::Keyboard(event) => match event { - keyboard::Event::KeyPressed { - key, - modifiers, - text, - .. - } if state.is_focused => { - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - return edit(Edit::Enter); - } - keyboard::Key::Named(key::Named::Backspace) => { - return edit(Edit::Backspace); - } - keyboard::Key::Named(key::Named::Delete) => { - return edit(Edit::Delete); - } - keyboard::Key::Named(key::Named::Escape) => { - return Some(Self::Unfocus); - } - keyboard::Key::Character("c") - if modifiers.command() => - { - return Some(Self::Copy); - } - keyboard::Key::Character("x") - if modifiers.command() => - { - return Some(Self::Cut); - } - keyboard::Key::Character("v") - if modifiers.command() && !modifiers.alt() => - { - return Some(Self::Paste); - } - keyboard::Key::Character("a") - if modifiers.command() => - { - return Some(Self::Action(Action::SelectAll)); - } - _ => {} - } + Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers, + text, + .. + }) => { + let status = if state.is_focused { + Status::Focused + } else { + Status::Active + }; - if let Some(text) = text { - if let Some(c) = text.chars().find(|c| !c.is_control()) - { - return edit(Edit::Insert(c)); - } - } - - if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key) { - let motion = if modifiers.macos_command() { - match motion { - Motion::Left => Motion::Home, - Motion::Right => Motion::End, - _ => motion, - } - } else { - motion - }; - - let motion = if modifiers.jump() { - motion.widen() - } else { - motion - }; - - return action(if modifiers.shift() { - Action::Select(motion) - } else { - Action::Move(motion) - }); - } - } - - None + if let Some(key_binding) = key_binding { + key_binding(KeyPress { + key, + modifiers, + text, + status, + }) + } else { + Binding::from_key_press(KeyPress { + key, + modifiers, + text, + status, + }) } - _ => None, - }, + .map(Self::Binding) + } _ => None, } } From e73c8b0413e7b32c13373388fe835bff74644a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Fri, 26 Jul 2024 10:43:36 +0200 Subject: [PATCH 150/657] Reduce `KeyPress` duplication in `text_editor` --- widget/src/text_editor.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index fa7863da..56fb7578 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1017,20 +1017,17 @@ impl<Message> Update<Message> { Status::Active }; + let key_press = KeyPress { + key, + modifiers, + text, + status, + }; + if let Some(key_binding) = key_binding { - key_binding(KeyPress { - key, - modifiers, - text, - status, - }) + key_binding(key_press) } else { - Binding::from_key_press(KeyPress { - key, - modifiers, - text, - status, - }) + Binding::from_key_press(key_press) } .map(Self::Binding) } From 23a7e9f981728e8a95039db8eb8e9f3d8c6ba3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 27 Jul 2024 18:23:12 +0200 Subject: [PATCH 151/657] Use `container::dark` style for `markdown` code blocks --- 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 0e47d35c..9cfd3c33 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -409,7 +409,7 @@ where ) .width(Length::Fill) .padding(spacing.0 / 2.0) - .style(container::rounded_box) + .style(container::dark) .into(), }); From ddcf02f9d0377afe6a35dbbb09a29b4bd52efe2e Mon Sep 17 00:00:00 2001 From: Cory Forsstrom <cforsstrom18@gmail.com> Date: Tue, 23 Jul 2024 13:36:40 -0700 Subject: [PATCH 152/657] Add background styling to span / rich text --- core/src/renderer/null.rs | 4 +++ core/src/text.rs | 39 ++++++++++++++++++++++++- core/src/text/paragraph.rs | 6 +++- examples/markdown/src/main.rs | 9 ++++-- graphics/src/text/paragraph.rs | 53 +++++++++++++++++++++++++++++++++- widget/src/markdown.rs | 24 ++++++++++----- widget/src/text/rich.rs | 22 ++++++++++++-- 7 files changed, 141 insertions(+), 16 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index d8d3c50a..5c7513c6 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -111,6 +111,10 @@ impl text::Paragraph for () { fn hit_span(&self, _point: Point) -> Option<usize> { None } + + fn span_bounds(&self, _index: usize) -> Vec<Rectangle> { + vec![] + } } impl text::Editor for () { diff --git a/core/src/text.rs b/core/src/text.rs index aa24d85f..0bdc6851 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -8,7 +8,7 @@ pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; -use crate::{Color, Pixels, Point, Rectangle, Size}; +use crate::{Border, Color, Pixels, Point, Rectangle, Size}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; @@ -235,6 +235,8 @@ pub struct Span<'a, Link = (), Font = crate::Font> { pub font: Option<Font>, /// The [`Color`] of the [`Span`]. pub color: Option<Color>, + /// The [`Background`] of the [`Span`]. + pub background: Option<Background>, /// The link of the [`Span`]. pub link: Option<Link>, } @@ -248,6 +250,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { line_height: None, font: None, color: None, + background: None, link: None, } } @@ -288,6 +291,21 @@ impl<'a, Link, Font> Span<'a, Link, Font> { self } + /// Sets the [`Background`] of the [`Span`]. + pub fn background(mut self, background: impl Into<Background>) -> Self { + self.background = Some(background.into()); + self + } + + /// Sets the [`Background`] of the [`Span`], if any. + pub fn background_maybe( + mut self, + background: Option<impl Into<Background>>, + ) -> Self { + self.background = background.map(Into::into); + self + } + /// Sets the link of the [`Span`]. pub fn link(mut self, link: impl Into<Link>) -> Self { self.link = Some(link.into()); @@ -308,6 +326,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { line_height: self.line_height, font: self.font, color: self.color, + background: self.background, link: self.link, } } @@ -406,3 +425,21 @@ into_fragment!(isize); into_fragment!(f32); into_fragment!(f64); + +/// The background style of text +#[derive(Debug, Clone, Copy)] +pub struct Background { + /// The background [`Color`] + pub color: Color, + /// The background [`Border`] + pub border: Border, +} + +impl From<Color> for Background { + fn from(color: Color) -> Self { + Background { + color, + border: Border::default(), + } + } +} diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 26650793..04a97f35 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -1,7 +1,7 @@ //! Draw paragraphs. use crate::alignment; use crate::text::{Difference, Hit, Span, Text}; -use crate::{Point, Size}; +use crate::{Point, Rectangle, Size}; /// A text paragraph. pub trait Paragraph: Sized + Default { @@ -42,6 +42,10 @@ pub trait Paragraph: Sized + Default { /// that was hit. fn hit_span(&self, point: Point) -> Option<usize>; + /// Returns all bounds for the provided [`Span`] index of the [`Paragraph`]. + /// A [`Span`] can have multiple bounds for each line it's on. + fn span_bounds(&self, index: usize) -> Vec<Rectangle>; + /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 173cb389..db40d0b9 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -28,8 +28,11 @@ impl Markdown { ( Self { content: text_editor::Content::with_text(INITIAL_CONTENT), - items: markdown::parse(INITIAL_CONTENT, theme.palette()) - .collect(), + items: markdown::parse( + INITIAL_CONTENT, + theme.extended_palette(), + ) + .collect(), theme, }, widget::focus_next(), @@ -46,7 +49,7 @@ impl Markdown { if is_edit { self.items = markdown::parse( &self.content.text(), - self.theme.palette(), + self.theme.extended_palette(), ) .collect(); } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index da703ceb..540e669b 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -2,7 +2,7 @@ use crate::core; use crate::core::alignment; use crate::core::text::{Hit, Shaping, Span, Text}; -use crate::core::{Font, Point, Size}; +use crate::core::{Font, Point, Rectangle, Size}; use crate::text; use std::fmt; @@ -251,6 +251,57 @@ impl core::text::Paragraph for Paragraph { Some(glyph.metadata) } + fn span_bounds(&self, index: usize) -> Vec<Rectangle> { + let internal = self.internal(); + + let mut current_bounds = None; + + let mut bounds = internal + .buffer + .layout_runs() + .flat_map(|run| { + let line_top = run.line_top; + let line_height = run.line_height; + + run.glyphs + .iter() + .map(move |glyph| (line_top, line_height, glyph)) + }) + .skip_while(|(_, _, glyph)| glyph.metadata != index) + .take_while(|(_, _, glyph)| glyph.metadata == index) + .fold(vec![], |mut spans, (line_top, line_height, glyph)| { + let y = line_top + glyph.y; + + let new_bounds = || { + Rectangle::new( + Point::new(glyph.x, y), + Size::new( + glyph.w, + glyph.line_height_opt.unwrap_or(line_height), + ), + ) + }; + + match current_bounds.as_mut() { + None => { + current_bounds = Some(new_bounds()); + } + Some(bounds) if y != bounds.y => { + spans.push(*bounds); + *bounds = new_bounds(); + } + Some(bounds) => { + bounds.width += glyph.w; + } + } + + spans + }); + + bounds.extend(current_bounds); + bounds + } + fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { use unicode_segmentation::UnicodeSegmentation; diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 9cfd3c33..6cd8535e 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -4,9 +4,12 @@ //! in code blocks. //! //! Only the variants of [`Item`] are currently supported. +use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; -use crate::core::theme::{self, Theme}; +use crate::core::text::Background; +use crate::core::theme::palette; +use crate::core::theme::Theme; use crate::core::{self, Element, Length, Pixels}; use crate::{column, container, rich_text, row, scrollable, span, text}; @@ -34,10 +37,10 @@ pub enum Item { } /// Parse the given Markdown content. -pub fn parse( - markdown: &str, - palette: theme::Palette, -) -> impl Iterator<Item = Item> + '_ { +pub fn parse<'a>( + markdown: &'a str, + palette: &'a palette::Extended, +) -> impl Iterator<Item = Item> + 'a { struct List { start: Option<u64>, items: Vec<Vec<Item>>, @@ -247,7 +250,7 @@ pub fn parse( }; let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) + span.color(palette.primary.base.color).link(link.clone()) } else { span }; @@ -257,10 +260,15 @@ pub fn parse( None } pulldown_cmark::Event::Code(code) if !metadata && !table => { - let span = span(code.into_string()).font(Font::MONOSPACE); + let span = span(code.into_string()) + .font(Font::MONOSPACE) + .background(Background { + color: palette.background.weak.color, + border: border::rounded(2), + }); let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) + span.color(palette.primary.base.color).link(link.clone()) } else { span }; diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 9c0e2fac..832a3ae7 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -9,8 +9,8 @@ use crate::core::widget::text::{ }; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, - Shell, Size, Widget, + self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Widget, }; use std::borrow::Cow; @@ -246,6 +246,24 @@ where let style = theme.style(&self.class); + // Draw backgrounds + for (index, span) in self.spans.iter().enumerate() { + if let Some(background) = span.background { + let translation = layout.position() - Point::ORIGIN; + + for bounds in state.paragraph.span_bounds(index) { + renderer.fill_quad( + renderer::Quad { + bounds: bounds + translation, + border: background.border, + ..Default::default() + }, + background.color, + ); + } + } + } + text::draw( renderer, defaults, From 4dc7b9b9619010b50ec6df837bd945ff0f675781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 13:15:04 +0200 Subject: [PATCH 153/657] Use dark background for inline code in `markdown` widget --- examples/markdown/src/main.rs | 9 +++------ widget/src/markdown.rs | 14 +++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index db40d0b9..efe5b324 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -28,11 +28,8 @@ impl Markdown { ( Self { content: text_editor::Content::with_text(INITIAL_CONTENT), - items: markdown::parse( - INITIAL_CONTENT, - theme.extended_palette(), - ) - .collect(), + items: markdown::parse(INITIAL_CONTENT, &theme.palette()) + .collect(), theme, }, widget::focus_next(), @@ -49,7 +46,7 @@ impl Markdown { if is_edit { self.items = markdown::parse( &self.content.text(), - self.theme.extended_palette(), + &self.theme.palette(), ) .collect(); } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 6cd8535e..362aba67 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -8,9 +8,8 @@ use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; use crate::core::text::Background; -use crate::core::theme::palette; -use crate::core::theme::Theme; -use crate::core::{self, Element, Length, Pixels}; +use crate::core::theme::{self, Theme}; +use crate::core::{self, color, Color, Element, Length, Pixels}; use crate::{column, container, rich_text, row, scrollable, span, text}; pub use pulldown_cmark::HeadingLevel; @@ -39,7 +38,7 @@ pub enum Item { /// Parse the given Markdown content. pub fn parse<'a>( markdown: &'a str, - palette: &'a palette::Extended, + palette: &'a theme::Palette, ) -> impl Iterator<Item = Item> + 'a { struct List { start: Option<u64>, @@ -250,7 +249,7 @@ pub fn parse<'a>( }; let span = if let Some(link) = link.as_ref() { - span.color(palette.primary.base.color).link(link.clone()) + span.color(palette.primary).link(link.clone()) } else { span }; @@ -262,13 +261,14 @@ pub fn parse<'a>( pulldown_cmark::Event::Code(code) if !metadata && !table => { let span = span(code.into_string()) .font(Font::MONOSPACE) + .color(Color::WHITE) .background(Background { - color: palette.background.weak.color, + color: color!(0x111111), border: border::rounded(2), }); let span = if let Some(link) = link.as_ref() { - span.color(palette.primary.base.color).link(link.clone()) + span.color(palette.primary).link(link.clone()) } else { span }; From f7fe1edcbbfde71d801379805b4605ff36075b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 13:44:08 +0200 Subject: [PATCH 154/657] Improve ergonomics of `span` background highlighting --- core/src/text.rs | 85 ++++++++++++++++++++++++++++------------- widget/src/markdown.rs | 7 +--- widget/src/text/rich.rs | 6 +-- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/core/src/text.rs b/core/src/text.rs index 0bdc6851..d54c8e6c 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -8,7 +8,7 @@ pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; -use crate::{Border, Color, Pixels, Point, Rectangle, Size}; +use crate::{Background, Border, Color, Pixels, Point, Rectangle, Size}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; @@ -235,10 +235,19 @@ pub struct Span<'a, Link = (), Font = crate::Font> { pub font: Option<Font>, /// The [`Color`] of the [`Span`]. pub color: Option<Color>, - /// The [`Background`] of the [`Span`]. - pub background: Option<Background>, /// The link of the [`Span`]. pub link: Option<Link>, + /// The [`Highlight`] of the [`Span`]. + pub highlight: Option<Highlight>, +} + +/// A text highlight. +#[derive(Debug, Clone, Copy)] +pub struct Highlight { + /// The [`Background`] of the highlight. + pub background: Background, + /// The [`Border`] of the highlight. + pub border: Border, } impl<'a, Link, Font> Span<'a, Link, Font> { @@ -250,7 +259,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { line_height: None, font: None, color: None, - background: None, + highlight: None, link: None, } } @@ -292,9 +301,8 @@ impl<'a, Link, Font> Span<'a, Link, Font> { } /// Sets the [`Background`] of the [`Span`]. - pub fn background(mut self, background: impl Into<Background>) -> Self { - self.background = Some(background.into()); - self + pub fn background(self, background: impl Into<Background>) -> Self { + self.background_maybe(Some(background)) } /// Sets the [`Background`] of the [`Span`], if any. @@ -302,7 +310,48 @@ impl<'a, Link, Font> Span<'a, Link, Font> { mut self, background: Option<impl Into<Background>>, ) -> Self { - self.background = background.map(Into::into); + let Some(background) = background else { + return self; + }; + + match &mut self.highlight { + Some(highlight) => { + highlight.background = background.into(); + } + None => { + self.highlight = Some(Highlight { + background: background.into(), + border: Border::default(), + }); + } + } + + self + } + + /// Sets the [`Border`] of the [`Span`]. + pub fn border(self, border: impl Into<Border>) -> Self { + self.border_maybe(Some(border)) + } + + /// Sets the [`Border`] of the [`Span`], if any. + pub fn border_maybe(mut self, border: Option<impl Into<Border>>) -> Self { + let Some(border) = border else { + return self; + }; + + match &mut self.highlight { + Some(highlight) => { + highlight.border = border.into(); + } + None => { + self.highlight = Some(Highlight { + border: border.into(), + background: Background::Color(Color::TRANSPARENT), + }); + } + } + self } @@ -326,8 +375,8 @@ impl<'a, Link, Font> Span<'a, Link, Font> { line_height: self.line_height, font: self.font, color: self.color, - background: self.background, link: self.link, + highlight: self.highlight, } } } @@ -425,21 +474,3 @@ into_fragment!(isize); into_fragment!(f32); into_fragment!(f64); - -/// The background style of text -#[derive(Debug, Clone, Copy)] -pub struct Background { - /// The background [`Color`] - pub color: Color, - /// The background [`Border`] - pub border: Border, -} - -impl From<Color> for Background { - fn from(color: Color) -> Self { - Background { - color, - border: Border::default(), - } - } -} diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 362aba67..cb3e9cfc 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -7,7 +7,6 @@ use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; -use crate::core::text::Background; use crate::core::theme::{self, Theme}; use crate::core::{self, color, Color, Element, Length, Pixels}; use crate::{column, container, rich_text, row, scrollable, span, text}; @@ -262,10 +261,8 @@ pub fn parse<'a>( let span = span(code.into_string()) .font(Font::MONOSPACE) .color(Color::WHITE) - .background(Background { - color: color!(0x111111), - border: border::rounded(2), - }); + .background(color!(0x111111)) + .border(border::rounded(2)); let span = if let Some(link) = link.as_ref() { span.color(palette.primary).link(link.clone()) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 832a3ae7..f636b219 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -248,17 +248,17 @@ where // Draw backgrounds for (index, span) in self.spans.iter().enumerate() { - if let Some(background) = span.background { + if let Some(highlight) = span.highlight { let translation = layout.position() - Point::ORIGIN; for bounds in state.paragraph.span_bounds(index) { renderer.fill_quad( renderer::Quad { bounds: bounds + translation, - border: background.border, + border: highlight.border, ..Default::default() }, - background.color, + highlight.background, ); } } From 2796a6bc974d847580ab23c6a5f58db994883ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 13:56:39 +0200 Subject: [PATCH 155/657] Add `padding` to `text::Span` --- core/src/size.rs | 14 ++++++++++++++ core/src/text.rs | 40 ++++++++++++++++++++++++++++++---------- widget/src/markdown.rs | 3 ++- widget/src/text/rich.rs | 12 +++++++++++- 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/core/src/size.rs b/core/src/size.rs index d7459355..95089236 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -99,6 +99,20 @@ impl<T> From<Size<T>> for Vector<T> { } } +impl<T> std::ops::Add for Size<T> +where + T: std::ops::Add<Output = T>, +{ + type Output = Size<T>; + + fn add(self, rhs: Self) -> Self::Output { + Size { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + impl<T> std::ops::Sub for Size<T> where T: std::ops::Sub<Output = T>, diff --git a/core/src/text.rs b/core/src/text.rs index d54c8e6c..2f085bd8 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -8,7 +8,9 @@ pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; -use crate::{Background, Border, Color, Pixels, Point, Rectangle, Size}; +use crate::{ + Background, Border, Color, Padding, Pixels, Point, Rectangle, Size, +}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; @@ -239,6 +241,10 @@ pub struct Span<'a, Link = (), Font = crate::Font> { pub link: Option<Link>, /// The [`Highlight`] of the [`Span`]. pub highlight: Option<Highlight>, + /// The [`Padding`] of the [`Span`]. + /// + /// Currently, it only affects the bounds of the [`Highlight`]. + pub padding: Padding, } /// A text highlight. @@ -261,6 +267,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { color: None, highlight: None, link: None, + padding: Padding::ZERO, } } @@ -300,6 +307,18 @@ impl<'a, Link, Font> Span<'a, Link, Font> { self } + /// Sets the link of the [`Span`]. + pub fn link(mut self, link: impl Into<Link>) -> Self { + self.link = Some(link.into()); + self + } + + /// Sets the link of the [`Span`], if any. + pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self { + self.link = link.map(Into::into); + self + } + /// Sets the [`Background`] of the [`Span`]. pub fn background(self, background: impl Into<Background>) -> Self { self.background_maybe(Some(background)) @@ -355,15 +374,15 @@ impl<'a, Link, Font> Span<'a, Link, Font> { self } - /// Sets the link of the [`Span`]. - pub fn link(mut self, link: impl Into<Link>) -> Self { - self.link = Some(link.into()); - self - } - - /// Sets the link of the [`Span`], if any. - pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self { - self.link = link.map(Into::into); + /// Sets the [`Padding`] of the [`Span`]. + /// + /// It only affects the [`background`] and [`border`] of the + /// [`Span`], currently. + /// + /// [`background`]: Self::background + /// [`border`]: Self::border + pub fn padding(mut self, padding: impl Into<Padding>) -> Self { + self.padding = padding.into(); self } @@ -377,6 +396,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { color: self.color, link: self.link, highlight: self.highlight, + padding: self.padding, } } } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index cb3e9cfc..c5eeaea9 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -262,7 +262,8 @@ pub fn parse<'a>( .font(Font::MONOSPACE) .color(Color::WHITE) .background(color!(0x111111)) - .border(border::rounded(2)); + .border(border::rounded(2)) + .padding(padding::left(2).right(2)); let span = if let Some(link) = link.as_ref() { span.color(palette.primary).link(link.clone()) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index f636b219..67db31db 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -10,7 +10,7 @@ use crate::core::widget::text::{ use crate::core::widget::tree::{self, Tree}; use crate::core::{ self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Widget, + Rectangle, Shell, Size, Vector, Widget, }; use std::borrow::Cow; @@ -252,6 +252,16 @@ where let translation = layout.position() - Point::ORIGIN; for bounds in state.paragraph.span_bounds(index) { + let bounds = Rectangle::new( + bounds.position() + - Vector::new(span.padding.left, span.padding.top), + bounds.size() + + Size::new( + span.padding.horizontal(), + span.padding.vertical(), + ), + ); + renderer.fill_quad( renderer::Quad { bounds: bounds + translation, From 2d69464846e6e1a7c59f78d894d8801ff82c5929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 13:59:11 +0200 Subject: [PATCH 156/657] Make `markdown::parse` take a `Palette` value --- examples/markdown/src/main.rs | 4 ++-- widget/src/markdown.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index efe5b324..173cb389 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -28,7 +28,7 @@ impl Markdown { ( Self { content: text_editor::Content::with_text(INITIAL_CONTENT), - items: markdown::parse(INITIAL_CONTENT, &theme.palette()) + items: markdown::parse(INITIAL_CONTENT, theme.palette()) .collect(), theme, }, @@ -46,7 +46,7 @@ impl Markdown { if is_edit { self.items = markdown::parse( &self.content.text(), - &self.theme.palette(), + self.theme.palette(), ) .collect(); } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index c5eeaea9..9cd4a62f 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -35,10 +35,10 @@ pub enum Item { } /// Parse the given Markdown content. -pub fn parse<'a>( - markdown: &'a str, - palette: &'a theme::Palette, -) -> impl Iterator<Item = Item> + 'a { +pub fn parse( + markdown: &str, + palette: theme::Palette, +) -> impl Iterator<Item = Item> + '_ { struct List { start: Option<u64>, items: Vec<Vec<Item>>, From 2e4c55bbffb45e7112e753fd77d78071acb252b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 14:17:59 +0200 Subject: [PATCH 157/657] Use `for` loop instead of `fold` in `span_bounds` --- graphics/src/text/paragraph.rs | 54 +++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 540e669b..b9f9c833 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -254,9 +254,10 @@ impl core::text::Paragraph for Paragraph { fn span_bounds(&self, index: usize) -> Vec<Rectangle> { let internal = self.internal(); + let mut bounds = Vec::new(); let mut current_bounds = None; - let mut bounds = internal + let glyphs = internal .buffer .layout_runs() .flat_map(|run| { @@ -268,35 +269,34 @@ impl core::text::Paragraph for Paragraph { .map(move |glyph| (line_top, line_height, glyph)) }) .skip_while(|(_, _, glyph)| glyph.metadata != index) - .take_while(|(_, _, glyph)| glyph.metadata == index) - .fold(vec![], |mut spans, (line_top, line_height, glyph)| { - let y = line_top + glyph.y; + .take_while(|(_, _, glyph)| glyph.metadata == index); - let new_bounds = || { - Rectangle::new( - Point::new(glyph.x, y), - Size::new( - glyph.w, - glyph.line_height_opt.unwrap_or(line_height), - ), - ) - }; + for (line_top, line_height, glyph) in glyphs { + let y = line_top + glyph.y; - match current_bounds.as_mut() { - None => { - current_bounds = Some(new_bounds()); - } - Some(bounds) if y != bounds.y => { - spans.push(*bounds); - *bounds = new_bounds(); - } - Some(bounds) => { - bounds.width += glyph.w; - } + let new_bounds = || { + Rectangle::new( + Point::new(glyph.x, y), + Size::new( + glyph.w, + glyph.line_height_opt.unwrap_or(line_height), + ), + ) + }; + + match current_bounds.as_mut() { + None => { + current_bounds = Some(new_bounds()); } - - spans - }); + Some(current_bounds) if y != current_bounds.y => { + bounds.push(*current_bounds); + *current_bounds = new_bounds(); + } + Some(current_bounds) => { + current_bounds.width += glyph.w; + } + } + } bounds.extend(current_bounds); bounds From 41a7318e5df3a49bf6e7fc2110155f2f22ff7e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 14:19:24 +0200 Subject: [PATCH 158/657] Remove comment in `text::Rich::draw` --- widget/src/text/rich.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 67db31db..9935e6c5 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -246,7 +246,6 @@ where let style = theme.style(&self.class); - // Draw backgrounds for (index, span) in self.spans.iter().enumerate() { if let Some(highlight) = span.highlight { let translation = layout.position() - Point::ORIGIN; From bf16d1ddcdcac21a4f4ad5ba79caba857067ee56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 15:09:54 +0200 Subject: [PATCH 159/657] Implement `underline` support for `rich_text` spans --- core/src/text.rs | 10 ++++++ widget/src/markdown.rs | 4 ++- widget/src/text/rich.rs | 77 ++++++++++++++++++++++++++++++----------- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/core/src/text.rs b/core/src/text.rs index 2f085bd8..68c586f1 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -245,6 +245,8 @@ pub struct Span<'a, Link = (), Font = crate::Font> { /// /// Currently, it only affects the bounds of the [`Highlight`]. pub padding: Padding, + /// Whether the [`Span`] should be underlined or not. + pub underline: bool, } /// A text highlight. @@ -268,6 +270,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { highlight: None, link: None, padding: Padding::ZERO, + underline: false, } } @@ -386,6 +389,12 @@ impl<'a, Link, Font> Span<'a, Link, Font> { self } + /// Sets whether the [`Span`] shoud be underlined or not. + pub fn underline(mut self, underline: bool) -> Self { + self.underline = underline; + self + } + /// Turns the [`Span`] into a static one. pub fn to_static(self) -> Span<'static, Link, Font> { Span { @@ -397,6 +406,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { link: self.link, highlight: self.highlight, padding: self.padding, + underline: self.underline, } } } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 9cd4a62f..dbdb6e42 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -248,7 +248,9 @@ pub fn parse( }; let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) + span.color(palette.primary) + .link(link.clone()) + .underline(true) } else { span }; diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 9935e6c5..8e4b0b7e 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -237,7 +237,7 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - _cursor_position: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let state = tree @@ -247,28 +247,65 @@ where let style = theme.style(&self.class); for (index, span) in self.spans.iter().enumerate() { - if let Some(highlight) = span.highlight { + if span.highlight.is_some() || span.underline { let translation = layout.position() - Point::ORIGIN; + let regions = state.paragraph.span_bounds(index); - for bounds in state.paragraph.span_bounds(index) { - let bounds = Rectangle::new( - bounds.position() - - Vector::new(span.padding.left, span.padding.top), - bounds.size() - + Size::new( - span.padding.horizontal(), - span.padding.vertical(), - ), - ); + if let Some(highlight) = span.highlight { + for bounds in ®ions { + let bounds = Rectangle::new( + bounds.position() + - Vector::new( + span.padding.left, + span.padding.top, + ), + bounds.size() + + Size::new( + span.padding.horizontal(), + span.padding.vertical(), + ), + ); - renderer.fill_quad( - renderer::Quad { - bounds: bounds + translation, - border: highlight.border, - ..Default::default() - }, - highlight.background, - ); + renderer.fill_quad( + renderer::Quad { + bounds: bounds + translation, + border: highlight.border, + ..Default::default() + }, + highlight.background, + ); + } + } + + if span.underline { + let line_height = span + .line_height + .unwrap_or(self.line_height) + .to_absolute( + span.size + .or(self.size) + .unwrap_or(renderer.default_size()), + ); + + for bounds in regions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + + translation + + Vector::new( + 0.0, + line_height.0 * 0.8 + 1.0, + ), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + span.color + .or(style.color) + .unwrap_or(defaults.text_color), + ); + } } } } From ca31dcadd52b3be05bcf01aa0426bf4279ac5f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 15:10:33 +0200 Subject: [PATCH 160/657] Underline `rich_text` links when hovered --- widget/src/markdown.rs | 4 +--- widget/src/text/rich.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index dbdb6e42..9cd4a62f 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -248,9 +248,7 @@ pub fn parse( }; let span = if let Some(link) = link.as_ref() { - span.color(palette.primary) - .link(link.clone()) - .underline(true) + span.color(palette.primary).link(link.clone()) } else { span }; diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 8e4b0b7e..d179c2d6 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -237,7 +237,7 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - _cursor: mouse::Cursor, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let state = tree @@ -246,8 +246,15 @@ where let style = theme.style(&self.class); + let hovered_span = cursor + .position_in(layout.bounds()) + .and_then(|position| state.paragraph.hit_span(position)); + for (index, span) in self.spans.iter().enumerate() { - if span.highlight.is_some() || span.underline { + let is_hovered_link = + span.link.is_some() && Some(index) == hovered_span; + + if span.highlight.is_some() || span.underline || is_hovered_link { let translation = layout.position() - Point::ORIGIN; let regions = state.paragraph.span_bounds(index); @@ -277,7 +284,7 @@ where } } - if span.underline { + if span.underline || is_hovered_link { let line_height = span .line_height .unwrap_or(self.line_height) From 9ce55eb51113b79d57b981ccd971242528a36395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 15:40:22 +0200 Subject: [PATCH 161/657] Make `underline` positioning aware of `line_height` --- widget/src/text/rich.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index d179c2d6..096056d4 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -285,14 +285,15 @@ where } if span.underline || is_hovered_link { + let size = span + .size + .or(self.size) + .unwrap_or(renderer.default_size()); + let line_height = span .line_height .unwrap_or(self.line_height) - .to_absolute( - span.size - .or(self.size) - .unwrap_or(renderer.default_size()), - ); + .to_absolute(size); for bounds in regions { renderer.fill_quad( @@ -302,7 +303,10 @@ where + translation + Vector::new( 0.0, - line_height.0 * 0.8 + 1.0, + size.0 + + (line_height.0 - size.0) + / 2.0 + - size.0 * 0.08, ), Size::new(bounds.width, 1.0), ), From ca8ebb16a67095260e4e94109f82d3ac1603e927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 17:45:11 +0200 Subject: [PATCH 162/657] Implement `strikethrough` support for `rich_text` spans --- core/src/text.rs | 10 ++++++ widget/src/markdown.rs | 23 ++++++++++---- widget/src/text/rich.rs | 68 ++++++++++++++++++++++++++++------------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/core/src/text.rs b/core/src/text.rs index 68c586f1..436fee9a 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -247,6 +247,8 @@ pub struct Span<'a, Link = (), Font = crate::Font> { pub padding: Padding, /// Whether the [`Span`] should be underlined or not. pub underline: bool, + /// Whether the [`Span`] should be struck through or not. + pub strikethrough: bool, } /// A text highlight. @@ -271,6 +273,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { link: None, padding: Padding::ZERO, underline: false, + strikethrough: false, } } @@ -395,6 +398,12 @@ impl<'a, Link, Font> Span<'a, Link, Font> { self } + /// Sets whether the [`Span`] shoud be struck through or not. + pub fn strikethrough(mut self, strikethrough: bool) -> Self { + self.strikethrough = strikethrough; + self + } + /// Turns the [`Span`] into a static one. pub fn to_static(self) -> Span<'static, Link, Font> { Span { @@ -407,6 +416,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> { highlight: self.highlight, padding: self.padding, underline: self.underline, + strikethrough: self.strikethrough, } } } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 9cd4a62f..23e36435 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -47,6 +47,7 @@ pub fn parse( let mut spans = Vec::new(); let mut strong = false; let mut emphasis = false; + let mut strikethrough = false; let mut metadata = false; let mut table = false; let mut link = None; @@ -59,7 +60,8 @@ pub fn parse( markdown, pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS - | pulldown_cmark::Options::ENABLE_TABLES, + | pulldown_cmark::Options::ENABLE_TABLES + | pulldown_cmark::Options::ENABLE_STRIKETHROUGH, ); let produce = |lists: &mut Vec<List>, item| { @@ -90,6 +92,10 @@ pub fn parse( emphasis = true; None } + pulldown_cmark::Tag::Strikethrough if !metadata && !table => { + strikethrough = true; + None + } pulldown_cmark::Tag::Link { dest_url, .. } if !metadata && !table => { @@ -155,12 +161,16 @@ pub fn parse( Item::Heading(level, spans.drain(..).collect()), ) } + pulldown_cmark::TagEnd::Strong if !metadata && !table => { + strong = false; + None + } pulldown_cmark::TagEnd::Emphasis if !metadata && !table => { emphasis = false; None } - pulldown_cmark::TagEnd::Strong if !metadata && !table => { - strong = false; + pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => { + strikethrough = false; None } pulldown_cmark::TagEnd::Link if !metadata && !table => { @@ -227,7 +237,7 @@ pub fn parse( return None; } - let span = span(text.into_string()); + let span = span(text.into_string()).strikethrough(strikethrough); let span = if strong || emphasis { span.font(Font { @@ -263,7 +273,8 @@ pub fn parse( .color(Color::WHITE) .background(color!(0x111111)) .border(border::rounded(2)) - .padding(padding::left(2).right(2)); + .padding(padding::left(2).right(2)) + .strikethrough(strikethrough); let span = if let Some(link) = link.as_ref() { span.color(palette.primary).link(link.clone()) @@ -275,7 +286,7 @@ pub fn parse( None } pulldown_cmark::Event::SoftBreak if !metadata && !table => { - spans.push(span(" ")); + spans.push(span(" ").strikethrough(strikethrough)); None } pulldown_cmark::Event::HardBreak if !metadata && !table => { diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 096056d4..c6aa1e14 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -254,7 +254,11 @@ where let is_hovered_link = span.link.is_some() && Some(index) == hovered_span; - if span.highlight.is_some() || span.underline || is_hovered_link { + if span.highlight.is_some() + || span.underline + || span.strikethrough + || is_hovered_link + { let translation = layout.position() - Point::ORIGIN; let regions = state.paragraph.span_bounds(index); @@ -284,7 +288,7 @@ where } } - if span.underline || is_hovered_link { + if span.underline || span.strikethrough || is_hovered_link { let size = span .size .or(self.size) @@ -295,27 +299,47 @@ where .unwrap_or(self.line_height) .to_absolute(size); - for bounds in regions { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle::new( - bounds.position() - + translation - + Vector::new( - 0.0, - size.0 - + (line_height.0 - size.0) - / 2.0 - - size.0 * 0.08, - ), - Size::new(bounds.width, 1.0), - ), - ..Default::default() - }, - span.color - .or(style.color) - .unwrap_or(defaults.text_color), + let color = span + .color + .or(style.color) + .unwrap_or(defaults.text_color); + + let baseline = translation + + Vector::new( + 0.0, + size.0 + (line_height.0 - size.0) / 2.0, ); + + if span.underline || is_hovered_link { + for bounds in ®ions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + baseline + - Vector::new(0.0, size.0 * 0.08), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + color, + ); + } + } + + if span.strikethrough { + for bounds in ®ions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + baseline + - Vector::new(0.0, size.0 / 2.0), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + color, + ); + } } } } From 1aa0a8fa0d8e5dfe9ab09a11ea7c69f71e19795b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 19:18:11 +0200 Subject: [PATCH 163/657] Enable Markdown highlighting in `markdown` example --- examples/markdown/overview.md | 27 +++++++++------------------ examples/markdown/src/main.rs | 10 +++++++++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/markdown/overview.md b/examples/markdown/overview.md index ca3250f1..66336c5b 100644 --- a/examples/markdown/overview.md +++ b/examples/markdown/overview.md @@ -1,18 +1,13 @@ # Overview -Inspired by [The Elm Architecture], Iced expects you to split user interfaces -into four different concepts: +Inspired by [The Elm Architecture], Iced expects you to split user interfaces into four different concepts: * __State__ — the state of your application -* __Messages__ — user interactions or meaningful events that you care - about -* __View logic__ — a way to display your __state__ as widgets that - may produce __messages__ on user interaction -* __Update logic__ — a way to react to __messages__ and update your - __state__ +* __Messages__ — user interactions or meaningful events that you care about +* __View logic__ — a way to display your __state__ as widgets that may produce __messages__ on user interaction +* __Update logic__ — a way to react to __messages__ and update your __state__ -We can build something to see how this works! Let's say we want a simple counter -that can be incremented and decremented using two buttons. +We can build something to see how this works! Let's say we want a simple counter that can be incremented and decremented using two buttons. We start by modelling the __state__ of our application: @@ -23,8 +18,7 @@ struct Counter { } ``` -Next, we need to define the possible user interactions of our counter: -the button presses. These interactions are our __messages__: +Next, we need to define the possible user interactions of our counter: the button presses. These interactions are our __messages__: ```rust #[derive(Debug, Clone, Copy)] @@ -34,8 +28,7 @@ pub enum Message { } ``` -Now, let's show the actual counter by putting it all together in our -__view logic__: +Now, let's show the actual counter by putting it all together in our __view logic__: ```rust use iced::widget::{button, column, text, Column}; @@ -59,8 +52,7 @@ impl Counter { } ``` -Finally, we need to be able to react to any produced __messages__ and change our -__state__ accordingly in our __update logic__: +Finally, we need to be able to react to any produced __messages__ and change our __state__ accordingly in our __update logic__: ```rust impl Counter { @@ -90,8 +82,7 @@ fn main() -> iced::Result { Iced will automatically: 1. Take the result of our __view logic__ and layout its widgets. - 1. Process events from our system and produce __messages__ for our - __update logic__. + 1. Process events from our system and produce __messages__ for our __update logic__. 1. Draw the resulting user interface. Read the [book], the [documentation], and the [examples] to learn more! diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 173cb389..ade6e453 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -1,3 +1,4 @@ +use iced::highlighter::{self, Highlighter}; use iced::widget::{self, markdown, row, scrollable, text_editor}; use iced::{Element, Fill, Font, Task, Theme}; @@ -63,7 +64,14 @@ impl Markdown { .on_action(Message::Edit) .height(Fill) .padding(10) - .font(Font::MONOSPACE); + .font(Font::MONOSPACE) + .highlight::<Highlighter>( + highlighter::Settings { + theme: highlighter::Theme::Base16Ocean, + token: "markdown".to_owned(), + }, + |highlight, _theme| highlight.to_format(), + ); let preview = markdown(&self.items, markdown::Settings::default()) .map(Message::LinkClicked); From 16212eaf52657ea47bba7add7deac378d0bf4b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 28 Jul 2024 23:59:51 +0200 Subject: [PATCH 164/657] Simplify `highlight` method for `text_editor` widget --- examples/editor/src/main.rs | 21 ++++++++------------- examples/markdown/src/main.rs | 10 ++-------- widget/src/text_editor.rs | 23 +++++++++++++++++++++-- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 9ffb4d1a..155e74a1 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,4 +1,4 @@ -use iced::highlighter::{self, Highlighter}; +use iced::highlighter; use iced::keyboard; use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, @@ -186,18 +186,13 @@ impl Editor { text_editor(&self.content) .height(Fill) .on_action(Message::ActionPerformed) - .highlight::<Highlighter>( - highlighter::Settings { - theme: self.theme, - token: self - .file - .as_deref() - .and_then(Path::extension) - .and_then(ffi::OsStr::to_str) - .map(str::to_string) - .unwrap_or(String::from("rs")), - }, - |highlight, _theme| highlight.to_format() + .highlight( + self.file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .unwrap_or("rs"), + self.theme, ), status, ] diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index ade6e453..eb51f985 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -1,4 +1,4 @@ -use iced::highlighter::{self, Highlighter}; +use iced::highlighter; use iced::widget::{self, markdown, row, scrollable, text_editor}; use iced::{Element, Fill, Font, Task, Theme}; @@ -65,13 +65,7 @@ impl Markdown { .height(Fill) .padding(10) .font(Font::MONOSPACE) - .highlight::<Highlighter>( - highlighter::Settings { - theme: highlighter::Theme::Base16Ocean, - token: "markdown".to_owned(), - }, - |highlight, _theme| highlight.to_format(), - ); + .highlight("markdown", highlighter::Theme::Base16Ocean); let preview = markdown(&self.items, markdown::Settings::default()) .map(Message::LinkClicked); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 56fb7578..b5092012 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -13,7 +13,7 @@ use crate::core::text::{self, LineHeight, Text}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ - Background, Border, Color, Element, Length, Padding, Pixels, Point, + self, Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; @@ -146,9 +146,28 @@ where self } + /// Highlights the [`TextEditor`] using the given syntax and theme. + #[cfg(feature = "highlighter")] + pub fn highlight( + self, + syntax: &str, + theme: iced_highlighter::Theme, + ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer> + where + Renderer: text::Renderer<Font = core::Font>, + { + self.highlight_with::<iced_highlighter::Highlighter>( + iced_highlighter::Settings { + theme, + token: syntax.to_owned(), + }, + |highlight, _theme| highlight.to_format(), + ) + } + /// Highlights the [`TextEditor`] with the given [`Highlighter`] and /// a strategy to turn its highlights into some text format. - pub fn highlight<H: text::Highlighter>( + pub fn highlight_with<H: text::Highlighter>( self, settings: H::Settings, to_format: fn( From d1fa9537f6066b8e93a156bd51a9cafc7c0bec17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 29 Jul 2024 00:02:34 +0200 Subject: [PATCH 165/657] Fix unused `core` import in `text_editor` module --- widget/src/text_editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index b5092012..899dd22a 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -13,7 +13,7 @@ use crate::core::text::{self, LineHeight, Text}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::{ - self, Background, Border, Color, Element, Length, Padding, Pixels, Point, + Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; @@ -154,7 +154,7 @@ where theme: iced_highlighter::Theme, ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer> where - Renderer: text::Renderer<Font = core::Font>, + Renderer: text::Renderer<Font = crate::core::Font>, { self.highlight_with::<iced_highlighter::Highlighter>( iced_highlighter::Settings { From 695721e120a173817125265a35afbc49813db716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 29 Jul 2024 00:51:46 +0200 Subject: [PATCH 166/657] Implement blinking cursor for `text_editor` --- widget/src/text_editor.rs | 103 +++++++++++++++++++++++++++++++++----- widget/src/text_input.rs | 15 ------ 2 files changed, 90 insertions(+), 28 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 899dd22a..bc391be3 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -10,8 +10,10 @@ use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight, Text}; +use crate::core::time::{Duration, Instant}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; +use crate::core::window; use crate::core::{ Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, @@ -369,7 +371,7 @@ where /// The state of a [`TextEditor`]. #[derive(Debug)] pub struct State<Highlighter: text::Highlighter> { - is_focused: bool, + focus: Option<Focus>, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, partial_scroll: f32, @@ -378,10 +380,39 @@ pub struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } +#[derive(Debug, Clone, Copy)] +struct Focus { + updated_at: Instant, + now: Instant, + is_window_focused: bool, +} + +impl Focus { + const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + + fn now() -> Self { + let now = Instant::now(); + + Self { + updated_at: now, + now, + is_window_focused: true, + } + } + + fn is_cursor_visible(&self) -> bool { + self.is_window_focused + && ((self.now - self.updated_at).as_millis() + / Self::CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0 + } +} + impl<Highlighter: text::Highlighter> State<Highlighter> { /// Returns whether the [`TextEditor`] is currently focused or not. pub fn is_focused(&self) -> bool { - self.is_focused + self.focus.is_some() } } @@ -389,15 +420,21 @@ impl<Highlighter: text::Highlighter> operation::Focusable for State<Highlighter> { fn is_focused(&self) -> bool { - self.is_focused + self.focus.is_some() } fn focus(&mut self) { - self.is_focused = true; + let now = Instant::now(); + + self.focus = Some(Focus { + updated_at: now, + now, + is_window_focused: true, + }); } fn unfocus(&mut self) { - self.is_focused = false; + self.focus = None; } } @@ -414,7 +451,7 @@ where fn state(&self) -> widget::tree::State { widget::tree::State::new(State { - is_focused: false, + focus: None, last_click: None, drag_click: None, partial_scroll: 0.0, @@ -502,6 +539,41 @@ where let state = tree.state.downcast_mut::<State<Highlighter>>(); + match event { + Event::Window(window::Event::Unfocused) => { + if let Some(focus) = &mut state.focus { + focus.is_window_focused = false; + } + } + Event::Window(window::Event::Focused) => { + if let Some(focus) = &mut state.focus { + focus.is_window_focused = true; + focus.updated_at = Instant::now(); + + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Window(window::Event::RedrawRequested(now)) => { + if let Some(focus) = &mut state.focus { + if focus.is_window_focused { + focus.now = now; + + let millis_until_redraw = + Focus::CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % Focus::CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis( + millis_until_redraw as u64, + ), + )); + } + } + } + _ => {} + } + let Some(update) = Update::from_event( event, state, @@ -523,7 +595,7 @@ where mouse::click::Kind::Triple => Action::SelectLine, }; - state.is_focused = true; + state.focus = Some(Focus::now()); state.last_click = Some(click); state.drag_click = Some(click.kind()); @@ -566,7 +638,7 @@ where match binding { Binding::Unfocus => { - state.is_focused = false; + state.focus = None; state.drag_click = None; } Binding::Copy => { @@ -645,6 +717,10 @@ where clipboard, shell, ); + + if let Some(focus) = &mut state.focus { + focus.updated_at = Instant::now(); + } } } @@ -679,7 +755,7 @@ where let status = if is_disabled { Status::Disabled - } else if state.is_focused { + } else if state.focus.is_some() { Status::Focused } else if is_mouse_over { Status::Hovered @@ -740,9 +816,9 @@ where bounds.y + self.padding.top, ); - if state.is_focused { + if let Some(focus) = state.focus.as_ref() { match internal.editor.cursor() { - Cursor::Caret(position) => { + Cursor::Caret(position) if focus.is_cursor_visible() => { let cursor = Rectangle::new( position + translation, @@ -784,6 +860,7 @@ where ); } } + Cursor::Caret(_) => {} } } } @@ -990,7 +1067,7 @@ impl<Message> Update<Message> { ); Some(Update::Click(click)) - } else if state.is_focused { + } else if state.focus.is_some() { binding(Binding::Unfocus) } else { None @@ -1030,7 +1107,7 @@ impl<Message> Update<Message> { text, .. }) => { - let status = if state.is_focused { + let status = if state.focus.is_some() { Status::Focused } else { Status::Active diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index a0fe14a0..20e80ba5 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1210,21 +1210,6 @@ impl<P: text::Paragraph> State<P> { Self::default() } - /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused() -> Self { - Self { - value: paragraph::Plain::default(), - placeholder: paragraph::Plain::default(), - icon: paragraph::Plain::default(), - is_focused: None, - is_dragging: false, - is_pasting: None, - last_click: None, - cursor: Cursor::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - } - } - /// Returns whether the [`TextInput`] is currently focused or not. pub fn is_focused(&self) -> bool { self.is_focused.is_some() From 6734d183594ebf89b8e6c030ea69d53ecb6b72db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 29 Jul 2024 00:54:23 +0200 Subject: [PATCH 167/657] Simplify `focus` method in `text_editor` --- widget/src/text_editor.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index bc391be3..a264ba06 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -424,13 +424,7 @@ impl<Highlighter: text::Highlighter> operation::Focusable } fn focus(&mut self) { - let now = Instant::now(); - - self.focus = Some(Focus { - updated_at: now, - now, - is_window_focused: true, - }); + self.focus = Some(Focus::now()); } fn unfocus(&mut self) { From 3eed34fa6f7ca9442c2bee8b93bb7465d6984259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 29 Jul 2024 21:26:03 +0200 Subject: [PATCH 168/657] Snap `Quad` lines to the pixel grid in `iced_wgpu` --- wgpu/src/shader/quad/solid.wgsl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index d908afbc..8eee16bb 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -30,6 +30,15 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { var pos: vec2<f32> = (input.pos + min(input.shadow_offset, vec2<f32>(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; var scale: vec2<f32> = (input.scale + vec2<f32>(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; + var snap: vec2<f32> = vec2<f32>(0.0, 0.0); + + if input.scale.x == 1.0 { + snap.x = round(pos.x) - pos.x; + } + + if input.scale.y == 1.0 { + snap.y = round(pos.y) - pos.y; + } var min_border_radius = min(input.scale.x, input.scale.y) * 0.5; var border_radius: vec4<f32> = vec4<f32>( @@ -43,13 +52,13 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0) + vec4<f32>(pos - vec2<f32>(0.5, 0.5) + snap, 0.0, 1.0) ); out.position = globals.transform * transform * vec4<f32>(vertex_position(input.vertex_index), 0.0, 1.0); out.color = input.color; out.border_color = input.border_color; - out.pos = input.pos * globals.scale; + out.pos = input.pos * globals.scale + snap; out.scale = input.scale * globals.scale; out.border_radius = border_radius * globals.scale; out.border_width = input.border_width * globals.scale; From 10f367a31375e127f61ed5c45b69d1e120af7e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 29 Jul 2024 23:00:16 +0200 Subject: [PATCH 169/657] Avoid exiting when a window is being opened Fixes #2532 --- winit/src/program.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 872c8a98..286fff77 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -650,7 +650,7 @@ async fn run_instance<P, C>( } = boot.try_recv().ok().flatten().expect("Receive boot"); let mut window_manager = WindowManager::new(); - let mut boot_window_closed = false; + let mut is_window_opening = !is_daemon; let mut events = Vec::new(); let mut messages = Vec::new(); @@ -706,6 +706,7 @@ async fn run_instance<P, C>( )); let _ = on_open.send(id); + is_window_opening = false; } Event::EventLoopAwakened(event) => { match event { @@ -742,6 +743,7 @@ async fn run_instance<P, C>( &mut user_interfaces, &mut window_manager, &mut ui_caches, + &mut is_window_opening, ); actions += 1; } @@ -926,16 +928,14 @@ async fn run_instance<P, C>( window_event, winit::event::WindowEvent::Destroyed ) + && !is_window_opening + && window_manager.is_empty() { - if boot_window_closed && window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); + control_sender + .start_send(Control::Exit) + .expect("Send control action"); - continue; - } - - boot_window_closed = true; + continue; } let Some((id, window)) = @@ -1153,6 +1153,7 @@ fn run_action<P, C>( >, window_manager: &mut WindowManager<P, C>, ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, + is_window_opening: &mut bool, ) where P: Program, C: Compositor<Renderer = P::Renderer> + 'static, @@ -1187,6 +1188,8 @@ fn run_action<P, C>( on_open: channel, }) .expect("Send control action"); + + *is_window_opening = true; } window::Action::Close(id) => { let _ = window_manager.remove(id); From 9be509d3b3301ee0b9b3379cd1e814fc3aa5e56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 30 Jul 2024 22:21:52 +0200 Subject: [PATCH 170/657] Reintroduce `Scrollable::with_direction` --- widget/src/scrollable.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 6dd593cb..9ba8c39b 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -49,12 +49,20 @@ where /// Creates a new vertical [`Scrollable`]. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self::with_direction(content, Direction::default()) + } + + /// Creates a new vertical [`Scrollable`]. + pub fn with_direction( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + direction: impl Into<Direction>, ) -> Self { Scrollable { id: None, width: Length::Shrink, height: Length::Shrink, - direction: Direction::default(), + direction: direction.into(), content: content.into(), on_scroll: None, class: Theme::default(), From 8f335757192701c4d276016ec68da6aa34b6c568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 30 Jul 2024 22:22:28 +0200 Subject: [PATCH 171/657] Expose additional `subscription` types in `advanced` --- src/advanced.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/advanced.rs b/src/advanced.rs index 57e40de6..843b381a 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -2,7 +2,8 @@ pub mod subscription { //! Write your own subscriptions. pub use crate::runtime::futures::subscription::{ - from_recipe, into_recipes, EventStream, Hasher, Recipe, + from_recipe, into_recipes, Event, EventStream, Hasher, MacOS, + PlatformSpecific, Recipe, }; } From fd593f8fb0c8476463f9c04ae2bcc96784b8c530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 30 Jul 2024 22:26:55 +0200 Subject: [PATCH 172/657] Return `window::Id` in `window::open` --- examples/multi_window/src/main.rs | 11 +++++++---- runtime/src/window.rs | 11 +++++++---- winit/src/program.rs | 5 +++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 3dcb58f5..ab09116e 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -40,12 +40,13 @@ enum Message { impl Example { fn new() -> (Self, Task<Message>) { + let (_id, open) = window::open(window::Settings::default()); + ( Self { windows: BTreeMap::new(), }, - window::open(window::Settings::default()) - .map(Message::WindowOpened), + open.map(Message::WindowOpened), ) } @@ -74,10 +75,12 @@ impl Example { }, ); - window::open(window::Settings { + let (_id, open) = window::open(window::Settings { position, ..window::Settings::default() - }) + }); + + open }) .map(Message::WindowOpened) } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index ee03f84f..cd27cdfe 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -218,12 +218,15 @@ pub fn close_requests() -> Subscription<Id> { /// Opens a new window with the given [`Settings`]; producing the [`Id`] /// of the new window on completion. -pub fn open(settings: Settings) -> Task<Id> { +pub fn open(settings: Settings) -> (Id, Task<Id>) { let id = Id::unique(); - task::oneshot(|channel| { - crate::Action::Window(Action::Open(id, settings, channel)) - }) + ( + id, + task::oneshot(|channel| { + crate::Action::Window(Action::Open(id, settings, channel)) + }), + ) } /// Closes the window with `id`. diff --git a/winit/src/program.rs b/winit/src/program.rs index 286fff77..7e7864b3 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -207,8 +207,9 @@ where let task = if let Some(window_settings) = window_settings { let mut task = Some(task); - runtime::window::open(window_settings) - .then(move |_| task.take().unwrap_or(Task::none())) + let (_id, open) = runtime::window::open(window_settings); + + open.then(move |_| task.take().unwrap_or(Task::none())) } else { task }; From 169667ef1b4fa754ed1edb5fa0e845aede2638fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 1 Aug 2024 19:24:30 +0200 Subject: [PATCH 173/657] Plug `received_url` in `winit::program` --- winit/src/program.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/winit/src/program.rs b/winit/src/program.rs index 7e7864b3..3d709b7e 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -414,6 +414,23 @@ where ); } + fn received_url( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + url: String, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::PlatformSpecific( + winit::event::PlatformSpecific::MacOS( + winit::event::MacOS::ReceivedUrl(url), + ), + ), + ), + ); + } + fn about_to_wait( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, From e84070acef84f883ca42d965c577e54ce60c3f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 3 Aug 2024 16:20:12 +0200 Subject: [PATCH 174/657] Implement `From<&Handle>` for `image::Handle` --- core/src/image.rs | 6 +++++ examples/screenshot/src/main.rs | 48 ++++++++++++++++++++------------- widget/src/image.rs | 2 +- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 82ecdd0f..77ff7500 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -101,6 +101,12 @@ where } } +impl From<&Handle> for Handle { + fn from(value: &Handle) -> Self { + value.clone() + } +} + impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 2d980dd9..5c105f6c 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -20,7 +20,7 @@ fn main() -> iced::Result { #[derive(Default)] struct Example { - screenshot: Option<Screenshot>, + screenshot: Option<(Screenshot, image::Handle)>, saved_png_path: Option<Result<String, PngError>>, png_saving: bool, crop_error: Option<screenshot::CropError>, @@ -52,10 +52,17 @@ impl Example { .map(Message::Screenshotted); } Message::Screenshotted(screenshot) => { - self.screenshot = Some(screenshot); + self.screenshot = Some(( + screenshot.clone(), + image::Handle::from_rgba( + screenshot.size.width, + screenshot.size.height, + screenshot.bytes, + ), + )); } Message::Png => { - if let Some(screenshot) = &self.screenshot { + if let Some((screenshot, _handle)) = &self.screenshot { self.png_saving = true; return Task::perform( @@ -81,7 +88,7 @@ impl Example { self.height_input_value = new_value; } Message::Crop => { - if let Some(screenshot) = &self.screenshot { + if let Some((screenshot, _handle)) = &self.screenshot { let cropped = screenshot.crop(Rectangle::<u32> { x: self.x_input_value.unwrap_or(0), y: self.y_input_value.unwrap_or(0), @@ -91,7 +98,14 @@ impl Example { match cropped { Ok(screenshot) => { - self.screenshot = Some(screenshot); + self.screenshot = Some(( + screenshot.clone(), + image::Handle::from_rgba( + screenshot.size.width, + screenshot.size.height, + screenshot.bytes, + ), + )); self.crop_error = None; } Err(crop_error) => { @@ -106,20 +120,16 @@ impl Example { } fn view(&self) -> Element<'_, Message> { - let image: Element<Message> = if let Some(screenshot) = &self.screenshot - { - image(image::Handle::from_rgba( - screenshot.size.width, - screenshot.size.height, - screenshot.clone(), - )) - .content_fit(ContentFit::Contain) - .width(Fill) - .height(Fill) - .into() - } else { - text("Press the button to take a screenshot!").into() - }; + let image: Element<Message> = + if let Some((_screenshot, handle)) = &self.screenshot { + image(handle) + .content_fit(ContentFit::Contain) + .width(Fill) + .height(Fill) + .into() + } else { + text("Press the button to take a screenshot!").into() + }; let image = container(image) .center_y(FillPortion(2)) diff --git a/widget/src/image.rs b/widget/src/image.rs index 80e17263..f1571400 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -43,7 +43,7 @@ pub struct Image<Handle> { impl<Handle> Image<Handle> { /// Creates a new [`Image`] with the given path. - pub fn new<T: Into<Handle>>(handle: T) -> Self { + pub fn new(handle: impl Into<Handle>) -> Self { Image { handle: handle.into(), width: Length::Shrink, From 87a613edd186461f1a8d224394043527a372571c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 3 Aug 2024 16:23:30 +0200 Subject: [PATCH 175/657] Render text on top of images by default --- tiny_skia/src/lib.rs | 20 +++++++++--------- wgpu/src/layer.rs | 2 +- wgpu/src/lib.rs | 48 ++++++++++++++++++++++---------------------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 1aabff00..6ec60158 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -178,6 +178,16 @@ impl Renderer { engine::adjust_clip_mask(clip_mask, clip_bounds); } + for image in &layer.images { + self.engine.draw_image( + image, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + for group in &layer.text { for text in group.as_slice() { self.engine.draw_text( @@ -190,16 +200,6 @@ impl Renderer { ); } } - - for image in &layer.images { - self.engine.draw_image( - image, - Transformation::scale(scale_factor), - pixels, - clip_mask, - clip_bounds, - ); - } } if !overlay.is_empty() { diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 9551311d..df289e0e 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -20,8 +20,8 @@ pub struct Layer { pub quads: quad::Batch, pub triangles: triangle::Batch, pub primitives: primitive::Batch, - pub text: text::Batch, pub images: image::Batch, + pub text: text::Batch, pending_meshes: Vec<Mesh>, pending_text: Vec<Text>, } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ad88ce3e..954340ec 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -182,19 +182,6 @@ impl Renderer { } } - if !layer.text.is_empty() { - engine.text_pipeline.prepare( - device, - queue, - &self.text_viewport, - encoder, - &mut self.text_storage, - &layer.text, - layer.bounds, - Transformation::scale(scale_factor), - ); - } - #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { engine.image_pipeline.prepare( @@ -207,6 +194,19 @@ impl Renderer { scale_factor, ); } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + &self.text_viewport, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + ); + } } } @@ -359,17 +359,6 @@ impl Renderer { )); } - if !layer.text.is_empty() { - text_layer += engine.text_pipeline.render( - &self.text_viewport, - &self.text_storage, - text_layer, - &layer.text, - scissor_rect, - &mut render_pass, - ); - } - #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { engine.image_pipeline.render( @@ -381,6 +370,17 @@ impl Renderer { image_layer += 1; } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_viewport, + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } } let _ = ManuallyDrop::into_inner(render_pass); From 4d849aaf0bded82b728b9470bb41203b49cc4db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maja=20K=C4=85dzio=C5=82ka?= <maya@compilercrim.es> Date: Sat, 3 Aug 2024 20:32:51 +0200 Subject: [PATCH 176/657] text_editor: Avoid rendering text outside the border If the height could fit slightly less than an extra line, said line would protrude beyond the border of the text editor. --- widget/src/text_editor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a264ba06..8b4b892d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -729,7 +729,7 @@ where defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, + _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -793,7 +793,7 @@ where }, position, style.placeholder, - *viewport, + bounds, ); } } else { @@ -801,7 +801,7 @@ where &internal.editor, position, defaults.text_color, - *viewport, + bounds, ); } From 0ceee1cf3ae49f5bd0e3f2b346a4b34076e4523a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 03:28:43 +0200 Subject: [PATCH 177/657] Implement image support for `canvas` widget --- core/src/rectangle.rs | 56 +++++++++++++++++++++ graphics/Cargo.toml | 1 + graphics/src/geometry/frame.rs | 72 ++++++++++++++++++++++++--- graphics/src/image.rs | 6 +++ renderer/src/fallback.rs | 42 ++++++++++++++-- tiny_skia/Cargo.toml | 2 +- tiny_skia/src/engine.rs | 1 + tiny_skia/src/geometry.rs | 86 ++++++++++++++++++++++++++++++-- tiny_skia/src/layer.rs | 43 ++++++++++++++++ tiny_skia/src/lib.rs | 11 +++- wgpu/Cargo.toml | 2 +- wgpu/src/geometry.rs | 91 ++++++++++++++++++++++++++++++++-- wgpu/src/image/mod.rs | 24 +++++++-- wgpu/src/layer.rs | 45 +++++++++++++++++ wgpu/src/lib.rs | 20 +++++++- wgpu/src/shader/image.wgsl | 12 ++++- 16 files changed, 485 insertions(+), 29 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 1556e072..99c8d55d 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -47,6 +47,62 @@ impl Rectangle<f32> { } } + /// Creates a new square [`Rectangle`] with the center at the origin and + /// with the given radius. + pub fn with_radius(radius: f32) -> Self { + Self { + x: -radius, + y: -radius, + width: radius * 2.0, + height: radius * 2.0, + } + } + + /// Creates a new axis-aligned [`Rectangle`] from the given vertices; returning the + /// rotation in [`Radians`] that must be applied to the axis-aligned [`Rectangle`] + /// to obtain the desired result. + pub fn with_vertices( + top_left: Point, + top_right: Point, + bottom_left: Point, + ) -> (Rectangle, Radians) { + let width = (top_right.x - top_left.x).hypot(top_right.y - top_left.y); + + let height = + (bottom_left.x - top_left.x).hypot(bottom_left.y - top_left.y); + + let rotation = + (top_right.y - top_left.y).atan2(top_right.x - top_left.x); + + let rotation = if rotation < 0.0 { + 2.0 * std::f32::consts::PI + rotation + } else { + rotation + }; + + let position = { + let center = Point::new( + (top_right.x + bottom_left.x) / 2.0, + (top_right.y + bottom_left.y) / 2.0, + ); + + let rotation = -rotation - std::f32::consts::PI * 2.0; + + Point::new( + center.x + (top_left.x - center.x) * rotation.cos() + - (top_left.y - center.y) * rotation.sin(), + center.y + + (top_left.x - center.x) * rotation.sin() + + (top_left.y - center.y) * rotation.cos(), + ) + }; + + ( + Rectangle::new(position, Size::new(width, height)), + Radians(rotation), + ) + } + /// Returns the [`Point`] at the center of the [`Rectangle`]. pub fn center(&self) -> Point { Point::new(self.center_x(), self.center_y()) diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index e8d27d07..7e2d767b 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -20,6 +20,7 @@ all-features = true [features] geometry = ["lyon_path"] image = ["dep:image", "kamadak-exif"] +svg = [] web-colors = [] fira-sans = [] diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 377589d7..d53d1331 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -1,5 +1,7 @@ //! Draw and generate geometry. -use crate::core::{Point, Radians, Rectangle, Size, Vector}; +use crate::core::image; +use crate::core::svg; +use crate::core::{Color, Point, Radians, Rectangle, Size, Vector}; use crate::geometry::{self, Fill, Path, Stroke, Text}; /// The region of a surface that can be used to draw geometry. @@ -75,6 +77,25 @@ where self.raw.fill_text(text); } + /// Draws the given image on the [`Frame`] inside the given bounds. + #[cfg(feature = "image")] + pub fn draw_image( + &mut self, + handle: &image::Handle, + bounds: Rectangle, + filter_method: image::FilterMethod, + rotation: impl Into<Radians>, + opacity: f32, + ) { + self.raw.draw_image( + handle, + bounds, + filter_method, + rotation.into(), + opacity, + ); + } + /// Stores the current transform of the [`Frame`] and executes the given /// drawing operations, restoring the transform afterwards. /// @@ -116,8 +137,7 @@ where let mut frame = self.draft(region); let result = f(&mut frame); - - self.paste(frame, Point::new(region.x, region.y)); + self.paste(frame); result } @@ -134,8 +154,8 @@ where } /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. - fn paste(&mut self, frame: Self, at: Point) { - self.raw.paste(frame.raw, at); + fn paste(&mut self, frame: Self) { + self.raw.paste(frame.raw); } /// Applies a translation to the current transform of the [`Frame`]. @@ -186,7 +206,7 @@ pub trait Backend: Sized { fn scale_nonuniform(&mut self, scale: impl Into<Vector>); fn draft(&mut self, clip_bounds: Rectangle) -> Self; - fn paste(&mut self, frame: Self, at: Point); + fn paste(&mut self, frame: Self); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>); @@ -199,6 +219,24 @@ pub trait Backend: Sized { fill: impl Into<Fill>, ); + fn draw_image( + &mut self, + handle: &image::Handle, + bounds: Rectangle, + filter_method: image::FilterMethod, + rotation: Radians, + opacity: f32, + ); + + fn draw_svg( + &mut self, + handle: &svg::Handle, + bounds: Rectangle, + color: Option<Color>, + rotation: Radians, + opacity: f32, + ); + fn into_geometry(self) -> Self::Geometry; } @@ -231,7 +269,7 @@ impl Backend for () { fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {} fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} - fn paste(&mut self, _frame: Self, _at: Point) {} + fn paste(&mut self, _frame: Self) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {} @@ -246,4 +284,24 @@ impl Backend for () { } fn into_geometry(self) -> Self::Geometry {} + + fn draw_image( + &mut self, + _handle: &image::Handle, + _bounds: Rectangle, + _filter_method: image::FilterMethod, + _rotation: Radians, + _opacity: f32, + ) { + } + + fn draw_svg( + &mut self, + _handle: &svg::Handle, + _bounds: Rectangle, + _color: Option<Color>, + _rotation: Radians, + _opacity: f32, + ) { + } } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 318592be..0e8f2fe3 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -23,6 +23,12 @@ pub enum Image { /// The opacity of the image. opacity: f32, + + /// If set to `true`, the image will be snapped to the pixel grid. + /// + /// This can avoid graphical glitches, specially when using a + /// [`image::FilterMethod::Nearest`]. + snap: bool, }, /// A vector image. Vector { diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 6a169692..ddf7fd95 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -572,6 +572,42 @@ mod geometry { delegate!(self, frame, frame.fill_text(text)); } + fn draw_image( + &mut self, + handle: &iced_wgpu::core::image::Handle, + bounds: Rectangle, + filter_method: iced_wgpu::core::image::FilterMethod, + rotation: Radians, + opacity: f32, + ) { + delegate!( + self, + frame, + frame.draw_image( + handle, + bounds, + filter_method, + rotation, + opacity + ) + ); + } + + fn draw_svg( + &mut self, + handle: &iced_wgpu::core::svg::Handle, + bounds: Rectangle, + color: Option<iced_wgpu::core::Color>, + rotation: Radians, + opacity: f32, + ) { + delegate!( + self, + frame, + frame.draw_svg(handle, bounds, color, rotation, opacity) + ); + } + fn push_transform(&mut self) { delegate!(self, frame, frame.push_transform()); } @@ -587,13 +623,13 @@ mod geometry { } } - fn paste(&mut self, frame: Self, at: Point) { + fn paste(&mut self, frame: Self) { match (self, frame) { (Self::Primary(target), Self::Primary(source)) => { - target.paste(source, at); + target.paste(source); } (Self::Secondary(target), Self::Secondary(source)) => { - target.paste(source, at); + target.paste(source); } _ => unreachable!(), } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 32ead3e0..323233f0 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] image = ["iced_graphics/image"] -svg = ["resvg"] +svg = ["iced_graphics/svg", "resvg"] geometry = ["iced_graphics/geometry"] [dependencies] diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 898657c8..c5c4d494 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -556,6 +556,7 @@ impl Engine { bounds, rotation, opacity, + snap: _, } => { let physical_bounds = *bounds * _transformation; diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 02b6e1b9..398b54f7 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,10 +1,12 @@ +use crate::core::image; +use crate::core::svg; use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::core::{Color, Pixels, Point, Radians, Rectangle, Size, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Gradient, Text}; +use crate::graphics::{Gradient, Image, Text}; use crate::Primitive; use std::rc::Rc; @@ -13,6 +15,7 @@ use std::rc::Rc; pub enum Geometry { Live { text: Vec<Text>, + images: Vec<Image>, primitives: Vec<Primitive>, clip_bounds: Rectangle, }, @@ -22,6 +25,7 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { pub text: Rc<[Text]>, + pub images: Rc<[Image]>, pub primitives: Rc<[Primitive]>, pub clip_bounds: Rectangle, } @@ -37,10 +41,12 @@ impl Cached for Geometry { match self { Self::Live { primitives, + images, text, clip_bounds, } => Cache { primitives: Rc::from(primitives), + images: Rc::from(images), text: Rc::from(text), clip_bounds, }, @@ -55,6 +61,7 @@ pub struct Frame { transform: tiny_skia::Transform, stack: Vec<tiny_skia::Transform>, primitives: Vec<Primitive>, + images: Vec<Image>, text: Vec<Text>, } @@ -68,6 +75,7 @@ impl Frame { clip_bounds, stack: Vec::new(), primitives: Vec::new(), + images: Vec::new(), text: Vec::new(), transform: tiny_skia::Transform::from_translate( clip_bounds.x, @@ -238,7 +246,7 @@ impl geometry::frame::Backend for Frame { Self::with_clip(clip_bounds) } - fn paste(&mut self, frame: Self, _at: Point) { + fn paste(&mut self, frame: Self) { self.primitives.extend(frame.primitives); self.text.extend(frame.text); } @@ -269,10 +277,82 @@ impl geometry::frame::Backend for Frame { fn into_geometry(self) -> Geometry { Geometry::Live { primitives: self.primitives, + images: self.images, text: self.text, clip_bounds: self.clip_bounds, } } + + fn draw_image( + &mut self, + handle: &image::Handle, + bounds: Rectangle, + filter_method: image::FilterMethod, + rotation: Radians, + opacity: f32, + ) { + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + self.images.push(Image::Raster { + handle: handle.clone(), + filter_method, + bounds, + rotation: rotation + external_rotation, + opacity, + snap: false, + }); + } + + fn draw_svg( + &mut self, + handle: &svg::Handle, + bounds: Rectangle, + color: Option<Color>, + rotation: Radians, + opacity: f32, + ) { + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + self.images.push(Image::Vector { + handle: handle.clone(), + bounds, + color, + rotation: rotation + external_rotation, + opacity, + }); + } +} + +fn transform_rectangle( + rectangle: Rectangle, + transform: tiny_skia::Transform, +) -> (Rectangle, Radians) { + let mut top_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y, + }; + + let mut top_right = tiny_skia::Point { + x: rectangle.x + rectangle.width, + y: rectangle.y, + }; + + let mut bottom_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y + rectangle.height, + }; + + transform.map_point(&mut top_left); + transform.map_point(&mut top_right); + transform.map_point(&mut bottom_left); + + Rectangle::with_vertices( + Point::new(top_left.x, top_left.y), + Point::new(top_right.x, top_right.y), + Point::new(bottom_left.x, bottom_left.y), + ) } fn convert_path(path: &Path) -> Option<tiny_skia::Path> { diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 48fca1d8..9a169f46 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -116,6 +116,48 @@ impl Layer { } pub fn draw_image( + &mut self, + image: &Image, + transformation: Transformation, + ) { + match image { + Image::Raster { + handle, + filter_method, + bounds, + rotation, + opacity, + snap: _, + } => { + self.draw_raster( + handle.clone(), + *filter_method, + *bounds, + transformation, + *rotation, + *opacity, + ); + } + Image::Vector { + handle, + color, + bounds, + rotation, + opacity, + } => { + self.draw_svg( + handle.clone(), + *color, + *bounds, + transformation, + *rotation, + *opacity, + ); + } + } + } + + pub fn draw_raster( &mut self, handle: image::Handle, filter_method: image::FilterMethod, @@ -130,6 +172,7 @@ impl Layer { bounds: bounds * transformation, rotation, opacity, + snap: false, }; self.images.push(image); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 6ec60158..f09e5aa3 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -330,6 +330,7 @@ impl graphics::geometry::Renderer for Renderer { match geometry { Geometry::Live { primitives, + images, text, clip_bounds, } => { @@ -339,6 +340,10 @@ impl graphics::geometry::Renderer for Renderer { transformation, ); + for image in images { + layer.draw_image(&image, transformation); + } + layer.draw_text_group(text, clip_bounds, transformation); } Geometry::Cache(cache) => { @@ -348,6 +353,10 @@ impl graphics::geometry::Renderer for Renderer { transformation, ); + for image in cache.images.iter() { + layer.draw_image(image, transformation); + } + layer.draw_text_cache( cache.text, cache.clip_bounds, @@ -381,7 +390,7 @@ impl core::image::Renderer for Renderer { opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image( + layer.draw_raster( handle, filter_method, bounds, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 30545fa2..b13ecb36 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -20,7 +20,7 @@ all-features = true [features] geometry = ["iced_graphics/geometry", "lyon"] image = ["iced_graphics/image"] -svg = ["resvg/text"] +svg = ["iced_graphics/svg", "resvg/text"] web-colors = ["iced_graphics/web-colors"] webgl = ["wgpu/webgl"] diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index f6213e1d..cb629b3e 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,7 +1,9 @@ //! Build and draw geometry. +use crate::core::image; +use crate::core::svg; use crate::core::text::LineHeight; use crate::core::{ - Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, + Color, Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, }; use crate::graphics::cache::{self, Cached}; use crate::graphics::color; @@ -11,7 +13,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Text}; +use crate::graphics::{self, Image, Text}; use crate::text; use crate::triangle; @@ -19,16 +21,22 @@ use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; +use std::sync::Arc; #[derive(Debug)] pub enum Geometry { - Live { meshes: Vec<Mesh>, text: Vec<Text> }, + Live { + meshes: Vec<Mesh>, + images: Vec<Image>, + text: Vec<Text>, + }, Cached(Cache), } #[derive(Debug, Clone)] pub struct Cache { pub meshes: Option<triangle::Cache>, + pub images: Option<Arc<[Image]>>, pub text: Option<text::Cache>, } @@ -45,7 +53,17 @@ impl Cached for Geometry { previous: Option<Self::Cache>, ) -> Self::Cache { match self { - Self::Live { meshes, text } => { + Self::Live { + meshes, + images, + text, + } => { + let images = if images.is_empty() { + None + } else { + Some(Arc::from(images)) + }; + if let Some(mut previous) = previous { if let Some(cache) = &mut previous.meshes { cache.update(meshes); @@ -59,10 +77,13 @@ impl Cached for Geometry { previous.text = text::Cache::new(group, text); } + previous.images = images; + previous } else { Cache { meshes: triangle::Cache::new(meshes), + images, text: text::Cache::new(group, text), } } @@ -78,6 +99,7 @@ pub struct Frame { clip_bounds: Rectangle, buffers: BufferStack, meshes: Vec<Mesh>, + images: Vec<Image>, text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, @@ -96,6 +118,7 @@ impl Frame { clip_bounds: bounds, buffers: BufferStack::new(), meshes: Vec::new(), + images: Vec::new(), text: Vec::new(), transforms: Transforms { previous: Vec::new(), @@ -335,10 +358,11 @@ impl geometry::frame::Backend for Frame { Frame::with_clip(clip_bounds) } - fn paste(&mut self, frame: Frame, _at: Point) { + fn paste(&mut self, frame: Frame) { self.meshes .extend(frame.buffers.into_meshes(frame.clip_bounds)); + self.images.extend(frame.images); self.text.extend(frame.text); } @@ -348,9 +372,51 @@ impl geometry::frame::Backend for Frame { Geometry::Live { meshes: self.meshes, + images: self.images, text: self.text, } } + + fn draw_image( + &mut self, + handle: &image::Handle, + bounds: Rectangle, + filter_method: image::FilterMethod, + rotation: Radians, + opacity: f32, + ) { + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + self.images.push(Image::Raster { + handle: handle.clone(), + filter_method, + bounds, + rotation: rotation + external_rotation, + opacity, + snap: false, + }); + } + + fn draw_svg( + &mut self, + handle: &svg::Handle, + bounds: Rectangle, + color: Option<Color>, + rotation: Radians, + opacity: f32, + ) { + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + self.images.push(Image::Vector { + handle: handle.clone(), + color, + bounds, + rotation: rotation + external_rotation, + opacity, + }); + } } enum Buffer { @@ -518,6 +584,21 @@ impl Transform { gradient } + + fn transform_rectangle( + &self, + rectangle: Rectangle, + ) -> (Rectangle, Radians) { + let top_left = self.transform_point(rectangle.position()); + let top_right = self.transform_point( + rectangle.position() + Vector::new(rectangle.width, 0.0), + ); + let bottom_left = self.transform_point( + rectangle.position() + Vector::new(0.0, rectangle.height), + ); + + Rectangle::with_vertices(top_left, top_right, bottom_left) + } } struct GradientVertex2DBuilder { gradient: gradient::Packed, diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index daa2fe16..ea34e4ec 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -149,6 +149,8 @@ impl Pipeline { 6 => Float32x2, // Layer 7 => Sint32, + // Snap + 8 => Uint32, ), }], }, @@ -212,8 +214,6 @@ impl Pipeline { transformation: Transformation, scale: f32, ) { - let transformation = transformation * Transformation::scale(scale); - let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); let linear_instances: &mut Vec<Instance> = &mut Vec::new(); @@ -226,6 +226,7 @@ impl Pipeline { bounds, rotation, opacity, + snap, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -235,6 +236,7 @@ impl Pipeline { [bounds.width, bounds.height], f32::from(*rotation), *opacity, + *snap, atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -268,6 +270,7 @@ impl Pipeline { size, f32::from(*rotation), *opacity, + true, atlas_entry, nearest_instances, ); @@ -300,6 +303,7 @@ impl Pipeline { nearest_instances, linear_instances, transformation, + scale, ); self.prepare_layer += 1; @@ -375,9 +379,12 @@ impl Layer { nearest_instances: &[Instance], linear_instances: &[Instance], transformation: Transformation, + scale_factor: f32, ) { let uniforms = Uniforms { transform: transformation.into(), + scale_factor, + _padding: [0.0; 3], }; let bytes = bytemuck::bytes_of(&uniforms); @@ -492,6 +499,7 @@ struct Instance { _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, + _snap: u32, } impl Instance { @@ -502,6 +510,10 @@ impl Instance { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Uniforms { transform: [f32; 16], + scale_factor: f32, + // Uniforms must be aligned to their largest member, + // this uses a mat4x4<f32> which aligns to 16, so align to that + _padding: [f32; 3], } fn add_instances( @@ -509,6 +521,7 @@ fn add_instances( image_size: [f32; 2], rotation: f32, opacity: f32, + snap: bool, entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { @@ -525,6 +538,7 @@ fn add_instances( image_size, rotation, opacity, + snap, allocation, instances, ); @@ -554,8 +568,8 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, opacity, allocation, - instances, + position, center, size, rotation, opacity, snap, + allocation, instances, ); } } @@ -569,6 +583,7 @@ fn add_instance( size: [f32; 2], rotation: f32, opacity: f32, + snap: bool, allocation: &atlas::Allocation, instances: &mut Vec<Instance>, ) { @@ -591,6 +606,7 @@ fn add_instance( (height as f32 - 1.0) / atlas::SIZE as f32, ], _layer: layer as u32, + _snap: snap as u32, }; instances.push(instance); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index df289e0e..e714e281 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -113,6 +113,49 @@ impl Layer { } pub fn draw_image( + &mut self, + image: &Image, + transformation: Transformation, + ) { + match image { + Image::Raster { + handle, + filter_method, + bounds, + rotation, + opacity, + snap, + } => { + self.draw_raster( + handle.clone(), + *filter_method, + *bounds, + transformation, + *rotation, + *opacity, + *snap, + ); + } + Image::Vector { + handle, + color, + bounds, + rotation, + opacity, + } => { + self.draw_svg( + handle.clone(), + *color, + *bounds, + transformation, + *rotation, + *opacity, + ); + } + } + } + + pub fn draw_raster( &mut self, handle: crate::core::image::Handle, filter_method: crate::core::image::FilterMethod, @@ -120,6 +163,7 @@ impl Layer { transformation: Transformation, rotation: Radians, opacity: f32, + snap: bool, ) { let image = Image::Raster { handle, @@ -127,6 +171,7 @@ impl Layer { bounds: bounds * transformation, rotation, opacity, + snap, }; self.images.push(image); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 954340ec..24e60979 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -536,13 +536,14 @@ impl core::image::Renderer for Renderer { opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image( + layer.draw_raster( handle, filter_method, bounds, transformation, rotation, opacity, + true, ); } } @@ -593,8 +594,17 @@ impl graphics::geometry::Renderer for Renderer { let (layer, transformation) = self.layers.current_mut(); match geometry { - Geometry::Live { meshes, text } => { + Geometry::Live { + meshes, + images, + text, + } => { layer.draw_mesh_group(meshes, transformation); + + for image in images { + layer.draw_image(&image, transformation); + } + layer.draw_text_group(text, transformation); } Geometry::Cached(cache) => { @@ -602,6 +612,12 @@ impl graphics::geometry::Renderer for Renderer { layer.draw_mesh_cache(meshes, transformation); } + if let Some(images) = cache.images { + for image in images.iter() { + layer.draw_image(image, transformation); + } + } + if let Some(text) = cache.text { layer.draw_text_cache(text, transformation); } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 0eeb100f..bc922838 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -1,5 +1,6 @@ struct Globals { transform: mat4x4<f32>, + scale_factor: f32, } @group(0) @binding(0) var<uniform> globals: Globals; @@ -16,6 +17,7 @@ struct VertexInput { @location(5) atlas_pos: vec2<f32>, @location(6) atlas_scale: vec2<f32>, @location(7) layer: i32, + @location(8) snap: u32, } struct VertexOutput { @@ -38,7 +40,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.opacity = input.opacity; // Calculate the vertex position and move the center to the origin - v_pos = round(input.pos) + v_pos * input.scale - input.center; + v_pos = input.pos + v_pos * input.scale - input.center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); @@ -51,7 +53,13 @@ fn vs_main(input: VertexInput) -> VertexOutput { ); // Calculate the final position of the vertex - out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); + out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); + + if bool(input.snap) { + out.position = round(out.position); + } + + out.position = globals.transform * out.position; return out; } From 4f5de3bbe910a36163daa52af85d21461eff2f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 03:29:06 +0200 Subject: [PATCH 178/657] Showcase `canvas` image support in `solar_system` example --- examples/solar_system/Cargo.toml | 2 +- examples/solar_system/assets/earth.png | Bin 0 -> 91888 bytes examples/solar_system/assets/moon.png | Bin 0 -> 105100 bytes examples/solar_system/assets/sun.png | Bin 0 -> 114689 bytes examples/solar_system/src/main.rs | 60 ++++++++++++++++--------- 5 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 examples/solar_system/assets/earth.png create mode 100644 examples/solar_system/assets/moon.png create mode 100644 examples/solar_system/assets/sun.png diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index ca64da14..e2c18c50 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "canvas", "tokio"] +iced.features = ["debug", "canvas", "image", "tokio"] rand = "0.8.3" tracing-subscriber = "0.3" diff --git a/examples/solar_system/assets/earth.png b/examples/solar_system/assets/earth.png new file mode 100644 index 0000000000000000000000000000000000000000..e81321d9e5e8b9d3cfab7fdaf1b475db5d496537 GIT binary patch literal 91888 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 literal 0 HcmV?d00001 diff --git a/examples/solar_system/assets/moon.png b/examples/solar_system/assets/moon.png new file mode 100644 index 0000000000000000000000000000000000000000..03f10cb74914db5623a7fae16c61366410b21df6 GIT binary patch literal 105100 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 G2~7ZxT39sz literal 0 HcmV?d00001 diff --git a/examples/solar_system/assets/sun.png b/examples/solar_system/assets/sun.png new file mode 100644 index 0000000000000000000000000000000000000000..29a928a7ce5011b09f799f66fb66fc1eb6abbb46 GIT binary patch literal 114689 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 P7#KWV{an^LB{Ts5lJ`Mx literal 0 HcmV?d00001 diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index a6f1ba6f..1d420959 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,10 +7,9 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::mouse; -use iced::widget::canvas; -use iced::widget::canvas::gradient; use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{Geometry, Path}; +use iced::widget::{canvas, image}; use iced::window; use iced::{ Color, Element, Fill, Point, Rectangle, Renderer, Size, Subscription, @@ -66,6 +65,9 @@ impl SolarSystem { #[derive(Debug)] struct State { + sun: image::Handle, + earth: image::Handle, + moon: image::Handle, space_cache: canvas::Cache, system_cache: canvas::Cache, start: Instant, @@ -85,6 +87,15 @@ impl State { let size = window::Settings::default().size; State { + sun: image::Handle::from_bytes( + include_bytes!("../assets/sun.png").as_slice(), + ), + earth: image::Handle::from_bytes( + include_bytes!("../assets/earth.png").as_slice(), + ), + moon: image::Handle::from_bytes( + include_bytes!("../assets/moon.png").as_slice(), + ), space_cache: canvas::Cache::default(), system_cache: canvas::Cache::default(), start: now, @@ -132,6 +143,8 @@ impl<Message> canvas::Program<Message> for State { let background = self.space_cache.draw(renderer, bounds.size(), |frame| { + frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK); + let stars = Path::new(|path| { for (p, size) in &self.stars { path.rectangle(*p, Size::new(*size, *size)); @@ -144,17 +157,21 @@ impl<Message> canvas::Program<Message> for State { let system = self.system_cache.draw(renderer, bounds.size(), |frame| { let center = frame.center(); + frame.translate(Vector::new(center.x, center.y)); - let sun = Path::circle(center, Self::SUN_RADIUS); - let orbit = Path::circle(center, Self::ORBIT_RADIUS); + frame.draw_image( + &self.sun, + Rectangle::with_radius(Self::SUN_RADIUS), + image::FilterMethod::Linear, + 0, + 1.0, + ); - frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); + let orbit = Path::circle(Point::ORIGIN, Self::ORBIT_RADIUS); frame.stroke( &orbit, Stroke { - style: stroke::Style::Solid(Color::from_rgba8( - 0, 153, 255, 0.1, - )), + style: stroke::Style::Solid(Color::WHITE.scale_alpha(0.1)), width: 1.0, line_dash: canvas::LineDash { offset: 0, @@ -169,27 +186,28 @@ impl<Message> canvas::Program<Message> for State { + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32; frame.with_save(|frame| { - frame.translate(Vector::new(center.x, center.y)); frame.rotate(rotation); frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); - let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); - - let earth_fill = gradient::Linear::new( - Point::new(-Self::EARTH_RADIUS, 0.0), - Point::new(Self::EARTH_RADIUS, 0.0), - ) - .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0)) - .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47)); - - frame.fill(&earth, earth_fill); + frame.draw_image( + &self.earth, + Rectangle::with_radius(Self::EARTH_RADIUS), + image::FilterMethod::Linear, + rotation * 10.0, + 1.0, + ); frame.with_save(|frame| { frame.rotate(rotation * 10.0); frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); - frame.fill(&moon, Color::WHITE); + frame.draw_image( + &self.moon, + Rectangle::with_radius(Self::MOON_RADIUS), + image::FilterMethod::Linear, + 0, + 1.0, + ); }); }); }); From ed959023e96f9e5fca66b0bc6b3b3b2e13bdc359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 03:36:05 +0200 Subject: [PATCH 179/657] Remove unnecessary `with_save` calls in `solar_system` example --- examples/solar_system/src/main.rs | 40 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 1d420959..1f4e642b 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -185,31 +185,27 @@ impl<Message> canvas::Program<Message> for State { let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32 + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32; - frame.with_save(|frame| { - frame.rotate(rotation); - frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + frame.rotate(rotation); + frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); - frame.draw_image( - &self.earth, - Rectangle::with_radius(Self::EARTH_RADIUS), - image::FilterMethod::Linear, - rotation * 10.0, - 1.0, - ); + frame.draw_image( + &self.earth, + Rectangle::with_radius(Self::EARTH_RADIUS), + image::FilterMethod::Linear, + rotation * 10.0, + 1.0, + ); - frame.with_save(|frame| { - frame.rotate(rotation * 10.0); - frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); + frame.rotate(rotation * 10.0); + frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - frame.draw_image( - &self.moon, - Rectangle::with_radius(Self::MOON_RADIUS), - image::FilterMethod::Linear, - 0, - 1.0, - ); - }); - }); + frame.draw_image( + &self.moon, + Rectangle::with_radius(Self::MOON_RADIUS), + image::FilterMethod::Linear, + 0, + 1.0, + ); }); vec![background, system] From 2ad3cff72223de7291d9c42149765c458944aacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 03:37:29 +0200 Subject: [PATCH 180/657] Increase Earth's spin a bit in `solar_system` example --- examples/solar_system/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 1f4e642b..a4931465 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -192,7 +192,7 @@ impl<Message> canvas::Program<Message> for State { &self.earth, Rectangle::with_radius(Self::EARTH_RADIUS), image::FilterMethod::Linear, - rotation * 10.0, + rotation * 20.0, 1.0, ); From 974ae6d1e7cd9df6967762a6d308106f4fe03edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 03:39:53 +0200 Subject: [PATCH 181/657] Fix broken imports in `iced_renderer` --- renderer/src/fallback.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index ddf7fd95..73e91dc6 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -441,7 +441,9 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::core::image; + use crate::core::svg; + use crate::core::{Color, Point, Radians, Rectangle, Size, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; @@ -574,9 +576,9 @@ mod geometry { fn draw_image( &mut self, - handle: &iced_wgpu::core::image::Handle, + handle: &image::Handle, bounds: Rectangle, - filter_method: iced_wgpu::core::image::FilterMethod, + filter_method: image::FilterMethod, rotation: Radians, opacity: f32, ) { @@ -595,9 +597,9 @@ mod geometry { fn draw_svg( &mut self, - handle: &iced_wgpu::core::svg::Handle, + handle: &svg::Handle, bounds: Rectangle, - color: Option<iced_wgpu::core::Color>, + color: Option<Color>, rotation: Radians, opacity: f32, ) { From 92bd3ecd6b4a6618f0fc725dea3694c3b40e5314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 04:30:12 +0200 Subject: [PATCH 182/657] Introduce `Image` struct in `core::image` --- core/src/image.rs | 79 +++++++++++++++++++++++++++---- core/src/lib.rs | 1 + core/src/renderer/null.rs | 14 ++---- examples/solar_system/src/main.rs | 15 ++---- graphics/src/geometry.rs | 1 + graphics/src/geometry/frame.rs | 39 ++------------- graphics/src/image.rs | 28 ++--------- renderer/src/fallback.rs | 48 +++---------------- tiny_skia/src/engine.rs | 17 ++----- tiny_skia/src/geometry.rs | 35 +++++--------- tiny_skia/src/layer.rs | 57 +++++++--------------- tiny_skia/src/lib.rs | 22 ++------- wgpu/src/geometry.rs | 35 +++++--------- wgpu/src/image/mod.rs | 19 +++----- wgpu/src/layer.rs | 51 +++++--------------- wgpu/src/lib.rs | 23 ++------- widget/src/canvas.rs | 4 +- widget/src/image.rs | 15 +++--- widget/src/image/viewer.rs | 15 +++--- 19 files changed, 184 insertions(+), 334 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 77ff7500..99d7f3ef 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -7,6 +7,73 @@ use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; +/// A raster image that can be drawn. +#[derive(Debug, Clone, PartialEq)] +pub struct Image<H = Handle> { + /// The handle of the image. + pub handle: H, + + /// The filter method of the image. + pub filter_method: FilterMethod, + + /// The rotation to be applied to the image, from its center. + pub rotation: Radians, + + /// The opacity of the image. + /// + /// 0 means transparent. 1 means opaque. + pub opacity: f32, + + /// If set to `true`, the image will be snapped to the pixel grid. + /// + /// This can avoid graphical glitches, specially when using a + /// [`FilterMethod::Nearest`]. + pub snap: bool, +} + +impl Image<Handle> { + /// Creates a new [`Image`] with the given handle. + pub fn new(handle: impl Into<Handle>) -> Self { + Self { + handle: handle.into(), + filter_method: FilterMethod::default(), + rotation: Radians(0.0), + opacity: 1.0, + snap: false, + } + } + + /// Sets the filter method of the [`Image`]. + pub fn filter_method(mut self, filter_method: FilterMethod) -> Self { + self.filter_method = filter_method; + self + } + + /// Sets the rotation of the [`Image`]. + pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Image`]. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); + self + } + + /// Sets whether the [`Image`] should be snapped to the pixel grid. + pub fn snap(mut self, snap: bool) -> Self { + self.snap = snap; + self + } +} + +impl From<&Handle> for Image { + fn from(handle: &Handle) -> Self { + Image::new(handle.clone()) + } +} + /// A handle of some image data. #[derive(Clone, PartialEq, Eq)] pub enum Handle { @@ -172,14 +239,6 @@ pub trait Renderer: crate::Renderer { /// Returns the dimensions of an image for the given [`Handle`]. fn measure_image(&self, handle: &Self::Handle) -> Size<u32>; - /// Draws an image with the given [`Handle`] and inside the provided - /// `bounds`. - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: FilterMethod, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ); + /// Draws an [`Image`] inside the provided `bounds`. + fn draw_image(&mut self, image: Image<Self::Handle>, bounds: Rectangle); } diff --git a/core/src/lib.rs b/core/src/lib.rs index 40a288e5..0e17d430 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -57,6 +57,7 @@ pub use element::Element; pub use event::Event; pub use font::Font; pub use gradient::Gradient; +pub use image::Image; pub use layout::Layout; pub use length::Length; pub use overlay::Overlay; diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 5c7513c6..e71117da 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -1,5 +1,5 @@ use crate::alignment; -use crate::image; +use crate::image::{self, Image}; use crate::renderer::{self, Renderer}; use crate::svg; use crate::text::{self, Text}; @@ -178,20 +178,14 @@ impl text::Editor for () { } impl image::Renderer for () { - type Handle = (); + type Handle = image::Handle; fn measure_image(&self, _handle: &Self::Handle) -> Size<u32> { Size::default() } - fn draw_image( - &mut self, - _handle: Self::Handle, - _filter_method: image::FilterMethod, - _bounds: Rectangle, - _rotation: Radians, - _opacity: f32, - ) { + fn draw_image(&mut self, _image: Image, _bounds: Rectangle) { + todo!() } } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index a4931465..9da9fd34 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -160,11 +160,8 @@ impl<Message> canvas::Program<Message> for State { frame.translate(Vector::new(center.x, center.y)); frame.draw_image( - &self.sun, Rectangle::with_radius(Self::SUN_RADIUS), - image::FilterMethod::Linear, - 0, - 1.0, + &self.sun, ); let orbit = Path::circle(Point::ORIGIN, Self::ORBIT_RADIUS); @@ -189,22 +186,16 @@ impl<Message> canvas::Program<Message> for State { frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); frame.draw_image( - &self.earth, Rectangle::with_radius(Self::EARTH_RADIUS), - image::FilterMethod::Linear, - rotation * 20.0, - 1.0, + canvas::Image::new(&self.earth).rotation(rotation * 20.0), ); frame.rotate(rotation * 10.0); frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); frame.draw_image( - &self.moon, Rectangle::with_radius(Self::MOON_RADIUS), - image::FilterMethod::Linear, - 0, - 1.0, + &self.moon, ); }); diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index ab4a7a36..c7515e46 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,6 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; +pub use crate::core::Image; pub use crate::gradient::{self, Gradient}; use crate::cache::Cached; diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index d53d1331..1a7af8e6 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -1,8 +1,7 @@ //! Draw and generate geometry. -use crate::core::image; use crate::core::svg; use crate::core::{Color, Point, Radians, Rectangle, Size, Vector}; -use crate::geometry::{self, Fill, Path, Stroke, Text}; +use crate::geometry::{self, Fill, Image, Path, Stroke, Text}; /// The region of a surface that can be used to draw geometry. #[allow(missing_debug_implementations)] @@ -79,21 +78,8 @@ where /// Draws the given image on the [`Frame`] inside the given bounds. #[cfg(feature = "image")] - pub fn draw_image( - &mut self, - handle: &image::Handle, - bounds: Rectangle, - filter_method: image::FilterMethod, - rotation: impl Into<Radians>, - opacity: f32, - ) { - self.raw.draw_image( - handle, - bounds, - filter_method, - rotation.into(), - opacity, - ); + pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + self.raw.draw_image(bounds, image); } /// Stores the current transform of the [`Frame`] and executes the given @@ -219,14 +205,7 @@ pub trait Backend: Sized { fill: impl Into<Fill>, ); - fn draw_image( - &mut self, - handle: &image::Handle, - bounds: Rectangle, - filter_method: image::FilterMethod, - rotation: Radians, - opacity: f32, - ); + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>); fn draw_svg( &mut self, @@ -285,15 +264,7 @@ impl Backend for () { fn into_geometry(self) -> Self::Geometry {} - fn draw_image( - &mut self, - _handle: &image::Handle, - _bounds: Rectangle, - _filter_method: image::FilterMethod, - _rotation: Radians, - _opacity: f32, - ) { - } + fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into<Image>) {} fn draw_svg( &mut self, diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 0e8f2fe3..2e4f4b5a 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -8,28 +8,8 @@ use crate::core::{image, svg, Color, Radians, Rectangle}; #[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, + Raster(image::Image, Rectangle), - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - - /// If set to `true`, the image will be snapped to the pixel grid. - /// - /// This can avoid graphical glitches, specially when using a - /// [`image::FilterMethod::Nearest`]. - snap: bool, - }, /// A vector image. Vector { /// The handle of a vector image. @@ -53,10 +33,8 @@ impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster { - bounds, rotation, .. - } - | Image::Vector { + Image::Raster(image, bounds) => bounds.rotate(image.rotation), + Image::Vector { bounds, rotation, .. } => bounds.rotate(*rotation), } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 73e91dc6..dc8a4107 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,7 +3,8 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Point, Radians, Rectangle, Size, Transformation, + self, Background, Color, Image, Point, Radians, Rectangle, Size, + Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -149,25 +150,8 @@ where delegate!(self, renderer, renderer.measure_image(handle)) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: image::FilterMethod, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - renderer, - renderer.draw_image( - handle, - filter_method, - bounds, - rotation, - opacity - ) - ); + fn draw_image(&mut self, image: Image<A::Handle>, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw_image(image, bounds)); } } @@ -441,11 +425,10 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::image; use crate::core::svg; use crate::core::{Color, Point, Radians, Rectangle, Size, Vector}; use crate::graphics::cache::{self, Cached}; - use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; + use crate::graphics::geometry::{self, Fill, Image, Path, Stroke, Text}; impl<A, B> geometry::Renderer for Renderer<A, B> where @@ -574,25 +557,8 @@ mod geometry { delegate!(self, frame, frame.fill_text(text)); } - fn draw_image( - &mut self, - handle: &image::Handle, - bounds: Rectangle, - filter_method: image::FilterMethod, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - frame, - frame.draw_image( - handle, - bounds, - filter_method, - rotation, - opacity - ) - ); + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + delegate!(self, frame, frame.draw_image(bounds, image)); } fn draw_svg( diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index c5c4d494..88e8a9b1 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -550,14 +550,7 @@ impl Engine { ) { match image { #[cfg(feature = "image")] - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - snap: _, - } => { + Image::Raster(raster, bounds) => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -568,7 +561,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(*rotation); + let radians = f32::from(raster.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -577,10 +570,10 @@ impl Engine { ); self.raster_pipeline.draw( - handle, - *filter_method, + &raster.handle, + raster.filter_method, *bounds, - *opacity, + raster.opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 398b54f7..7b0e68f4 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,12 +1,11 @@ -use crate::core::image; use crate::core::svg; use crate::core::text::LineHeight; use crate::core::{Color, Pixels, Point, Radians, Rectangle, Size, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; -use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Gradient, Image, Text}; +use crate::graphics::geometry::{self, Image, Path, Style}; +use crate::graphics::{self, Gradient, Text}; use crate::Primitive; use std::rc::Rc; @@ -15,7 +14,7 @@ use std::rc::Rc; pub enum Geometry { Live { text: Vec<Text>, - images: Vec<Image>, + images: Vec<graphics::Image>, primitives: Vec<Primitive>, clip_bounds: Rectangle, }, @@ -25,7 +24,7 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { pub text: Rc<[Text]>, - pub images: Rc<[Image]>, + pub images: Rc<[graphics::Image]>, pub primitives: Rc<[Primitive]>, pub clip_bounds: Rectangle, } @@ -61,7 +60,7 @@ pub struct Frame { transform: tiny_skia::Transform, stack: Vec<tiny_skia::Transform>, primitives: Vec<Primitive>, - images: Vec<Image>, + images: Vec<graphics::Image>, text: Vec<Text>, } @@ -283,25 +282,15 @@ impl geometry::frame::Backend for Frame { } } - fn draw_image( - &mut self, - handle: &image::Handle, - bounds: Rectangle, - filter_method: image::FilterMethod, - rotation: Radians, - opacity: f32, - ) { + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + let mut image = image.into(); + let (bounds, external_rotation) = transform_rectangle(bounds, self.transform); - self.images.push(Image::Raster { - handle: handle.clone(), - filter_method, - bounds, - rotation: rotation + external_rotation, - opacity, - snap: false, - }); + image.rotation += external_rotation; + + self.images.push(graphics::Image::Raster(image, bounds)); } fn draw_svg( @@ -315,7 +304,7 @@ impl geometry::frame::Backend for Frame { let (bounds, external_rotation) = transform_rectangle(bounds, self.transform); - self.images.push(Image::Vector { + self.images.push(graphics::Image::Vector { handle: handle.clone(), bounds, color, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 9a169f46..33df0a86 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,11 +1,12 @@ +use crate::core::renderer::Quad; +use crate::core::svg; use crate::core::{ - image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, - Transformation, + Background, Color, Image, Point, Radians, Rectangle, Transformation, }; +use crate::graphics; use crate::graphics::damage; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; -use crate::graphics::{self, Image}; use crate::Primitive; use std::rc::Rc; @@ -18,7 +19,7 @@ pub struct Layer { pub quads: Vec<(Quad, Background)>, pub primitives: Vec<Item<Primitive>>, pub text: Vec<Item<Text>>, - pub images: Vec<Image>, + pub images: Vec<graphics::Image>, } impl Layer { @@ -117,28 +118,14 @@ impl Layer { pub fn draw_image( &mut self, - image: &Image, + image: graphics::Image, transformation: Transformation, ) { match image { - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - snap: _, - } => { - self.draw_raster( - handle.clone(), - *filter_method, - *bounds, - transformation, - *rotation, - *opacity, - ); + graphics::Image::Raster(raster, bounds) => { + self.draw_raster(raster.clone(), bounds, transformation); } - Image::Vector { + graphics::Image::Vector { handle, color, bounds, @@ -147,11 +134,11 @@ impl Layer { } => { self.draw_svg( handle.clone(), - *color, - *bounds, + color, + bounds, transformation, - *rotation, - *opacity, + rotation, + opacity, ); } } @@ -159,21 +146,11 @@ impl Layer { pub fn draw_raster( &mut self, - handle: image::Handle, - filter_method: image::FilterMethod, + image: Image, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let image = Image::Raster { - handle, - filter_method, - bounds: bounds * transformation, - rotation, - opacity, - snap: false, - }; + let image = graphics::Image::Raster(image, bounds * transformation); self.images.push(image); } @@ -187,7 +164,7 @@ impl Layer { rotation: Radians, opacity: f32, ) { - let svg = Image::Vector { + let svg = graphics::Image::Vector { handle, color, bounds: bounds * transformation, @@ -304,7 +281,7 @@ impl Layer { &previous.images, ¤t.images, |image| vec![image.bounds().expand(1.0)], - Image::eq, + graphics::Image::eq, ); damage.extend(text); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index f09e5aa3..00864c11 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -341,7 +341,7 @@ impl graphics::geometry::Renderer for Renderer { ); for image in images { - layer.draw_image(&image, transformation); + layer.draw_image(image, transformation); } layer.draw_text_group(text, clip_bounds, transformation); @@ -354,7 +354,7 @@ impl graphics::geometry::Renderer for Renderer { ); for image in cache.images.iter() { - layer.draw_image(image, transformation); + layer.draw_image(image.clone(), transformation); } layer.draw_text_cache( @@ -381,23 +381,9 @@ impl core::image::Renderer for Renderer { self.engine.raster_pipeline.dimensions(handle) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: core::image::FilterMethod, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster( - handle, - filter_method, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_raster(image, bounds, transformation); } } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index cb629b3e..6b1bb074 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,5 +1,4 @@ //! Build and draw geometry. -use crate::core::image; use crate::core::svg; use crate::core::text::LineHeight; use crate::core::{ @@ -9,11 +8,11 @@ use crate::graphics::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - self, LineCap, LineDash, LineJoin, Path, Stroke, Style, + self, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Image, Text}; +use crate::graphics::{self, Text}; use crate::text; use crate::triangle; @@ -27,7 +26,7 @@ use std::sync::Arc; pub enum Geometry { Live { meshes: Vec<Mesh>, - images: Vec<Image>, + images: Vec<graphics::Image>, text: Vec<Text>, }, Cached(Cache), @@ -36,7 +35,7 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { pub meshes: Option<triangle::Cache>, - pub images: Option<Arc<[Image]>>, + pub images: Option<Arc<[graphics::Image]>>, pub text: Option<text::Cache>, } @@ -99,7 +98,7 @@ pub struct Frame { clip_bounds: Rectangle, buffers: BufferStack, meshes: Vec<Mesh>, - images: Vec<Image>, + images: Vec<graphics::Image>, text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, @@ -377,25 +376,15 @@ impl geometry::frame::Backend for Frame { } } - fn draw_image( - &mut self, - handle: &image::Handle, - bounds: Rectangle, - filter_method: image::FilterMethod, - rotation: Radians, - opacity: f32, - ) { + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + let mut image = image.into(); + let (bounds, external_rotation) = self.transforms.current.transform_rectangle(bounds); - self.images.push(Image::Raster { - handle: handle.clone(), - filter_method, - bounds, - rotation: rotation + external_rotation, - opacity, - snap: false, - }); + image.rotation += external_rotation; + + self.images.push(graphics::Image::Raster(image, bounds)); } fn draw_svg( @@ -409,7 +398,7 @@ impl geometry::frame::Backend for Frame { let (bounds, external_rotation) = self.transforms.current.transform_rectangle(bounds); - self.images.push(Image::Vector { + self.images.push(graphics::Image::Vector { handle: handle.clone(), color, bounds, diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index ea34e4ec..2b0d6251 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -220,25 +220,18 @@ impl Pipeline { for image in images { match &image { #[cfg(feature = "image")] - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - snap, - } => { + Image::Raster(image, bounds) => { if let Some(atlas_entry) = - cache.upload_raster(device, encoder, handle) + cache.upload_raster(device, encoder, &image.handle) { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], - f32::from(*rotation), - *opacity, - *snap, + f32::from(image.rotation), + image.opacity, + image.snap, atlas_entry, - match filter_method { + match image.filter_method { crate::core::image::FilterMethod::Nearest => { nearest_instances } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index e714e281..71fa0250 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,5 +1,6 @@ use crate::core::{ - renderer, Background, Color, Point, Radians, Rectangle, Transformation, + self, renderer, Background, Color, Point, Radians, Rectangle, + Transformation, }; use crate::graphics; use crate::graphics::color; @@ -112,29 +113,10 @@ impl Layer { self.pending_text.push(text); } - pub fn draw_image( - &mut self, - image: &Image, - transformation: Transformation, - ) { + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - snap, - } => { - self.draw_raster( - handle.clone(), - *filter_method, - *bounds, - transformation, - *rotation, - *opacity, - *snap, - ); + Image::Raster(image, bounds) => { + self.draw_raster(image, bounds, transformation); } Image::Vector { handle, @@ -145,11 +127,11 @@ impl Layer { } => { self.draw_svg( handle.clone(), - *color, - *bounds, + color, + bounds, transformation, - *rotation, - *opacity, + rotation, + opacity, ); } } @@ -157,22 +139,11 @@ impl Layer { pub fn draw_raster( &mut self, - handle: crate::core::image::Handle, - filter_method: crate::core::image::FilterMethod, + image: core::Image, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, - snap: bool, ) { - let image = Image::Raster { - handle, - filter_method, - bounds: bounds * transformation, - rotation, - opacity, - snap, - }; + let image = Image::Raster(image, bounds * transformation); self.images.push(image); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 24e60979..e5f45ad2 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -527,24 +527,9 @@ impl core::image::Renderer for Renderer { self.image_cache.borrow_mut().measure_image(handle) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: core::image::FilterMethod, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster( - handle, - filter_method, - bounds, - transformation, - rotation, - opacity, - true, - ); + layer.draw_raster(image, bounds, transformation); } } @@ -602,7 +587,7 @@ impl graphics::geometry::Renderer for Renderer { layer.draw_mesh_group(meshes, transformation); for image in images { - layer.draw_image(&image, transformation); + layer.draw_image(image, transformation); } layer.draw_text_group(text, transformation); @@ -613,7 +598,7 @@ impl graphics::geometry::Renderer for Renderer { } if let Some(images) = cache.images { - for image in images.iter() { + for image in images.iter().cloned() { layer.draw_image(image, transformation); } } diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 73cef087..185fa082 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -8,8 +8,8 @@ pub use program::Program; pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ - fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, - Path, Stroke, Style, Text, + fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash, + LineJoin, Path, Stroke, Style, Text, }; use crate::core; diff --git a/widget/src/image.rs b/widget/src/image.rs index f1571400..55dd9816 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -8,8 +8,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, - Vector, Widget, + self, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -181,11 +181,14 @@ pub fn draw<Renderer, Handle>( let render = |renderer: &mut Renderer| { renderer.draw_image( - handle.clone(), - filter_method, + core::Image { + handle: handle.clone(), + filter_method, + rotation: rotation.radians(), + opacity, + snap: true, + }, drawing_bounds, - rotation.radians(), - opacity, ); }; diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index b8b69b60..b1aad22c 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, ContentFit, Element, Layout, Length, Pixels, Point, Radians, - Rectangle, Shell, Size, Vector, Widget, + Clipboard, ContentFit, Element, Image, Layout, Length, Pixels, Point, + Radians, Rectangle, Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -349,11 +349,14 @@ where let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { renderer.draw_image( - self.handle.clone(), - self.filter_method, + Image { + handle: self.handle.clone(), + filter_method: self.filter_method, + rotation: Radians(0.0), + opacity: 1.0, + snap: true, + }, drawing_bounds, - Radians(0.0), - 1.0, ); }); }; From 3904f0b83af2fd3cd0d841d34d0d9c6193eeb845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 04:30:59 +0200 Subject: [PATCH 183/657] Remove `todo!` in `core::renderer::null` --- core/src/renderer/null.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e71117da..3c6f8be0 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -184,9 +184,7 @@ impl image::Renderer for () { Size::default() } - fn draw_image(&mut self, _image: Image, _bounds: Rectangle) { - todo!() - } + fn draw_image(&mut self, _image: Image, _bounds: Rectangle) {} } impl svg::Renderer for () { From 8708101c892540ffc966cf7ee9d66ca5cd2e8ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 04:33:30 +0200 Subject: [PATCH 184/657] Simplify types in `tiny_skia::layer` --- tiny_skia/src/layer.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 33df0a86..5d3cb07b 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,12 +1,12 @@ use crate::core::renderer::Quad; use crate::core::svg; use crate::core::{ - Background, Color, Image, Point, Radians, Rectangle, Transformation, + self, Background, Color, Point, Radians, Rectangle, Transformation, }; -use crate::graphics; use crate::graphics::damage; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; +use crate::graphics::{self, Image}; use crate::Primitive; use std::rc::Rc; @@ -19,7 +19,7 @@ pub struct Layer { pub quads: Vec<(Quad, Background)>, pub primitives: Vec<Item<Primitive>>, pub text: Vec<Item<Text>>, - pub images: Vec<graphics::Image>, + pub images: Vec<Image>, } impl Layer { @@ -73,7 +73,7 @@ impl Layer { pub fn draw_text( &mut self, - text: crate::core::Text, + text: core::Text, position: Point, color: Color, clip_bounds: Rectangle, @@ -116,16 +116,12 @@ impl Layer { .push(Item::Cached(text, clip_bounds, transformation)); } - pub fn draw_image( - &mut self, - image: graphics::Image, - transformation: Transformation, - ) { + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { - graphics::Image::Raster(raster, bounds) => { + Image::Raster(raster, bounds) => { self.draw_raster(raster.clone(), bounds, transformation); } - graphics::Image::Vector { + Image::Vector { handle, color, bounds, @@ -146,11 +142,11 @@ impl Layer { pub fn draw_raster( &mut self, - image: Image, + image: core::Image, bounds: Rectangle, transformation: Transformation, ) { - let image = graphics::Image::Raster(image, bounds * transformation); + let image = Image::Raster(image, bounds * transformation); self.images.push(image); } @@ -164,7 +160,7 @@ impl Layer { rotation: Radians, opacity: f32, ) { - let svg = graphics::Image::Vector { + let svg = Image::Vector { handle, color, bounds: bounds * transformation, @@ -281,7 +277,7 @@ impl Layer { &previous.images, ¤t.images, |image| vec![image.bounds().expand(1.0)], - graphics::Image::eq, + Image::eq, ); damage.extend(text); @@ -313,7 +309,7 @@ impl graphics::Layer for Layer { fn flush(&mut self) {} - fn resize(&mut self, bounds: graphics::core::Rectangle) { + fn resize(&mut self, bounds: Rectangle) { self.bounds = bounds; } From d4b08462e5a25929ec4df32f242898986902af56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 04:52:55 +0200 Subject: [PATCH 185/657] Introduce `Svg` struct in `core::svg` --- core/src/image.rs | 4 +- core/src/lib.rs | 1 + core/src/renderer/null.rs | 13 +------ core/src/svg.rs | 69 ++++++++++++++++++++++++++++++---- graphics/src/geometry.rs | 2 +- graphics/src/geometry/frame.rs | 28 +++----------- graphics/src/image.rs | 25 +++--------- renderer/src/fallback.rs | 36 +++--------------- tiny_skia/src/engine.rs | 16 +++----- tiny_skia/src/geometry.rs | 30 +++++---------- tiny_skia/src/layer.rs | 35 +++-------------- tiny_skia/src/lib.rs | 18 +-------- wgpu/src/geometry.rs | 40 ++++++++------------ wgpu/src/image/mod.rs | 19 +++++----- wgpu/src/layer.rs | 33 +++------------- wgpu/src/lib.rs | 18 +-------- widget/src/image.rs | 6 +-- widget/src/svg.rs | 10 +++-- 18 files changed, 146 insertions(+), 257 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 99d7f3ef..f985636a 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -16,7 +16,7 @@ pub struct Image<H = Handle> { /// The filter method of the image. pub filter_method: FilterMethod, - /// The rotation to be applied to the image, from its center. + /// The rotation to be applied to the image; on its center. pub rotation: Radians, /// The opacity of the image. @@ -26,7 +26,7 @@ pub struct Image<H = Handle> { /// If set to `true`, the image will be snapped to the pixel grid. /// - /// This can avoid graphical glitches, specially when using a + /// This can avoid graphical glitches, specially when using /// [`FilterMethod::Nearest`]. pub snap: bool, } diff --git a/core/src/lib.rs b/core/src/lib.rs index 0e17d430..df599f45 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -70,6 +70,7 @@ pub use rotation::Rotation; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; +pub use svg::Svg; pub use text::Text; pub use theme::Theme; pub use transformation::Transformation; diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 3c6f8be0..e3a07280 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -4,8 +4,7 @@ use crate::renderer::{self, Renderer}; use crate::svg; use crate::text::{self, Text}; use crate::{ - Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, - Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; impl Renderer for () { @@ -192,13 +191,5 @@ impl svg::Renderer for () { Size::default() } - fn draw_svg( - &mut self, - _handle: svg::Handle, - _color: Option<Color>, - _bounds: Rectangle, - _rotation: Radians, - _opacity: f32, - ) { - } + fn draw_svg(&mut self, _svg: svg::Svg, _bounds: Rectangle) {} } diff --git a/core/src/svg.rs b/core/src/svg.rs index 946b8156..ac19b223 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -7,6 +7,66 @@ use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; +/// A raster image that can be drawn. +#[derive(Debug, Clone, PartialEq)] +pub struct Svg<H = Handle> { + /// The handle of the [`Svg`]. + pub handle: H, + + /// The [`Color`] filter to be applied to the [`Svg`]. + /// + /// If some [`Color`] is set, the whole [`Svg`] will be + /// painted with it—ignoring any intrinsic colors. + /// + /// This can be useful for coloring icons programmatically + /// (e.g. with a theme). + pub color: Option<Color>, + + /// The rotation to be applied to the image; on its center. + pub rotation: Radians, + + /// The opacity of the [`Svg`]. + /// + /// 0 means transparent. 1 means opaque. + pub opacity: f32, +} + +impl Svg<Handle> { + /// Creates a new [`Svg`] with the given handle. + pub fn new(handle: impl Into<Handle>) -> Self { + Self { + handle: handle.into(), + color: None, + rotation: Radians(0.0), + opacity: 1.0, + } + } + + /// Sets the [`Color`] filter of the [`Svg`]. + pub fn color(mut self, color: impl Into<Color>) -> Self { + self.color = Some(color.into()); + self + } + + /// Sets the rotation of the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Svg`]. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); + self + } +} + +impl From<&Handle> for Svg { + fn from(handle: &Handle) -> Self { + Svg::new(handle.clone()) + } +} + /// A handle of Svg data. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { @@ -95,12 +155,5 @@ pub trait Renderer: crate::Renderer { fn measure_svg(&self, handle: &Handle) -> Size<u32>; /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw_svg( - &mut self, - handle: Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle); } diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index c7515e46..2b4b45a6 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,7 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; -pub use crate::core::Image; +pub use crate::core::{Image, Svg}; pub use crate::gradient::{self, Gradient}; use crate::cache::Cached; diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 1a7af8e6..f3c0817c 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -1,7 +1,6 @@ //! Draw and generate geometry. -use crate::core::svg; -use crate::core::{Color, Point, Radians, Rectangle, Size, Vector}; -use crate::geometry::{self, Fill, Image, Path, Stroke, Text}; +use crate::core::{Point, Radians, Rectangle, Size, Vector}; +use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text}; /// The region of a surface that can be used to draw geometry. #[allow(missing_debug_implementations)] @@ -206,15 +205,7 @@ pub trait Backend: Sized { ); fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>); - - fn draw_svg( - &mut self, - handle: &svg::Handle, - bounds: Rectangle, - color: Option<Color>, - rotation: Radians, - opacity: f32, - ); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>); fn into_geometry(self) -> Self::Geometry; } @@ -262,17 +253,8 @@ impl Backend for () { ) { } - fn into_geometry(self) -> Self::Geometry {} - fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into<Image>) {} + fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into<Svg>) {} - fn draw_svg( - &mut self, - _handle: &svg::Handle, - _bounds: Rectangle, - _color: Option<Color>, - _rotation: Radians, - _opacity: f32, - ) { - } + fn into_geometry(self) -> Self::Geometry {} } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 2e4f4b5a..67a5e0cf 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,7 +2,9 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::{image, svg, Color, Radians, Rectangle}; +use crate::core::image; +use crate::core::svg; +use crate::core::Rectangle; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] @@ -11,22 +13,7 @@ pub enum Image { Raster(image::Image, Rectangle), /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option<Color>, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, + Vector(svg::Svg, Rectangle), } impl Image { @@ -34,9 +21,7 @@ impl Image { pub fn bounds(&self) -> Rectangle { match self { Image::Raster(image, bounds) => bounds.rotate(image.rotation), - Image::Vector { - bounds, rotation, .. - } => bounds.rotate(*rotation), + Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), } } } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index dc8a4107..fbd285db 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,8 +3,7 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Image, Point, Radians, Rectangle, Size, - Transformation, + self, Background, Color, Image, Point, Rectangle, Size, Svg, Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -164,19 +163,8 @@ where delegate!(self, renderer, renderer.measure_svg(handle)) } - fn draw_svg( - &mut self, - handle: svg::Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - renderer, - renderer.draw_svg(handle, color, bounds, rotation, opacity) - ); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw_svg(svg, bounds)); } } @@ -425,8 +413,7 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::svg; - use crate::core::{Color, Point, Radians, Rectangle, Size, Vector}; + use crate::core::{Point, Radians, Rectangle, Size, Svg, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::{self, Fill, Image, Path, Stroke, Text}; @@ -561,19 +548,8 @@ mod geometry { delegate!(self, frame, frame.draw_image(bounds, image)); } - fn draw_svg( - &mut self, - handle: &svg::Handle, - bounds: Rectangle, - color: Option<Color>, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - frame, - frame.draw_svg(handle, bounds, color, rotation, opacity) - ); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + delegate!(self, frame, frame.draw_svg(bounds, svg)); } fn push_transform(&mut self) { diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 88e8a9b1..196c36cf 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -580,13 +580,7 @@ impl Engine { ); } #[cfg(feature = "svg")] - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { + Image::Vector(svg, bounds) => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -597,7 +591,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(*rotation); + let radians = f32::from(svg.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -606,10 +600,10 @@ impl Engine { ); self.vector_pipeline.draw( - handle, - *color, + &svg.handle, + svg.color, physical_bounds, - *opacity, + svg.opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 7b0e68f4..659612d1 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,11 +1,10 @@ -use crate::core::svg; use crate::core::text::LineHeight; -use crate::core::{Color, Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; -use crate::graphics::geometry::{self, Image, Path, Style}; -use crate::graphics::{self, Gradient, Text}; +use crate::graphics::geometry::{self, Path, Style}; +use crate::graphics::{self, Gradient, Image, Text}; use crate::Primitive; use std::rc::Rc; @@ -282,7 +281,7 @@ impl geometry::frame::Backend for Frame { } } - fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) { let mut image = image.into(); let (bounds, external_rotation) = @@ -293,24 +292,15 @@ impl geometry::frame::Backend for Frame { self.images.push(graphics::Image::Raster(image, bounds)); } - fn draw_svg( - &mut self, - handle: &svg::Handle, - bounds: Rectangle, - color: Option<Color>, - rotation: Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + let mut svg = svg.into(); + let (bounds, external_rotation) = transform_rectangle(bounds, self.transform); - self.images.push(graphics::Image::Vector { - handle: handle.clone(), - bounds, - color, - rotation: rotation + external_rotation, - opacity, - }); + svg.rotation += external_rotation; + + self.images.push(Image::Vector(svg, bounds)); } } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 5d3cb07b..bdfd4d38 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,7 +1,6 @@ use crate::core::renderer::Quad; -use crate::core::svg; use crate::core::{ - self, Background, Color, Point, Radians, Rectangle, Transformation, + self, Background, Color, Point, Rectangle, Svg, Transformation, }; use crate::graphics::damage; use crate::graphics::layer; @@ -119,23 +118,10 @@ impl Layer { pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { Image::Raster(raster, bounds) => { - self.draw_raster(raster.clone(), bounds, transformation); + self.draw_raster(raster, bounds, transformation); } - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { - self.draw_svg( - handle.clone(), - color, - bounds, - transformation, - rotation, - opacity, - ); + Image::Vector(svg, bounds) => { + self.draw_svg(svg, bounds, transformation); } } } @@ -153,20 +139,11 @@ impl Layer { pub fn draw_svg( &mut self, - handle: svg::Handle, - color: Option<Color>, + svg: Svg, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let svg = Image::Vector { - handle, - color, - bounds: bounds * transformation, - rotation, - opacity, - }; + let svg = Image::Vector(svg, bounds * transformation); self.images.push(svg); } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 00864c11..758921d4 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -396,23 +396,9 @@ impl core::svg::Renderer for Renderer { self.engine.vector_pipeline.viewport_dimensions(handle) } - fn draw_svg( - &mut self, - handle: core::svg::Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_svg(svg, bounds, transformation); } } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 6b1bb074..be65ba36 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,18 +1,17 @@ //! Build and draw geometry. -use crate::core::svg; use crate::core::text::LineHeight; use crate::core::{ - Color, Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, + self, Pixels, Point, Radians, Rectangle, Size, Svg, Transformation, Vector, }; use crate::graphics::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - self, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style, + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Text}; +use crate::graphics::{Image, Text}; use crate::text; use crate::triangle; @@ -26,7 +25,7 @@ use std::sync::Arc; pub enum Geometry { Live { meshes: Vec<Mesh>, - images: Vec<graphics::Image>, + images: Vec<Image>, text: Vec<Text>, }, Cached(Cache), @@ -35,7 +34,7 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { pub meshes: Option<triangle::Cache>, - pub images: Option<Arc<[graphics::Image]>>, + pub images: Option<Arc<[Image]>>, pub text: Option<text::Cache>, } @@ -98,7 +97,7 @@ pub struct Frame { clip_bounds: Rectangle, buffers: BufferStack, meshes: Vec<Mesh>, - images: Vec<graphics::Image>, + images: Vec<Image>, text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, @@ -292,7 +291,7 @@ impl geometry::frame::Backend for Frame { height: f32::INFINITY, }; - self.text.push(graphics::Text::Cached { + self.text.push(Text::Cached { content: text.content, bounds, color: text.color, @@ -376,7 +375,7 @@ impl geometry::frame::Backend for Frame { } } - fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) { let mut image = image.into(); let (bounds, external_rotation) = @@ -384,27 +383,18 @@ impl geometry::frame::Backend for Frame { image.rotation += external_rotation; - self.images.push(graphics::Image::Raster(image, bounds)); + self.images.push(Image::Raster(image, bounds)); } - fn draw_svg( - &mut self, - handle: &svg::Handle, - bounds: Rectangle, - color: Option<Color>, - rotation: Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + let mut svg = svg.into(); + let (bounds, external_rotation) = self.transforms.current.transform_rectangle(bounds); - self.images.push(graphics::Image::Vector { - handle: handle.clone(), - color, - bounds, - rotation: rotation + external_rotation, - opacity, - }); + svg.rotation += external_rotation; + + self.images.push(Image::Vector(svg, bounds)); } } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 2b0d6251..1b16022a 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -246,23 +246,22 @@ impl Pipeline { Image::Raster { .. } => {} #[cfg(feature = "svg")] - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { + Image::Vector(svg, bounds) => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, encoder, handle, *color, size, scale, + device, + encoder, + &svg.handle, + svg.color, + size, + scale, ) { add_instances( [bounds.x, bounds.y], size, - f32::from(*rotation), - *opacity, + f32::from(svg.rotation), + svg.opacity, true, atlas_entry, nearest_instances, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 71fa0250..68d5a015 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,6 +1,5 @@ use crate::core::{ - self, renderer, Background, Color, Point, Radians, Rectangle, - Transformation, + self, renderer, Background, Color, Point, Rectangle, Svg, Transformation, }; use crate::graphics; use crate::graphics::color; @@ -118,21 +117,8 @@ impl Layer { Image::Raster(image, bounds) => { self.draw_raster(image, bounds, transformation); } - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { - self.draw_svg( - handle.clone(), - color, - bounds, - transformation, - rotation, - opacity, - ); + Image::Vector(svg, bounds) => { + self.draw_svg(svg, bounds, transformation); } } } @@ -150,20 +136,11 @@ impl Layer { pub fn draw_svg( &mut self, - handle: crate::core::svg::Handle, - color: Option<Color>, + svg: Svg, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let svg = Image::Vector { - handle, - color, - bounds: bounds * transformation, - rotation, - opacity, - }; + let svg = Image::Vector(svg, bounds * transformation); self.images.push(svg); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index e5f45ad2..39167514 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -539,23 +539,9 @@ impl core::svg::Renderer for Renderer { self.image_cache.borrow_mut().measure_svg(handle) } - fn draw_svg( - &mut self, - handle: core::svg::Handle, - color_filter: Option<Color>, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color_filter, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_svg(svg, bounds, transformation); } } diff --git a/widget/src/image.rs b/widget/src/image.rs index 55dd9816..e04f2d6f 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -8,8 +8,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - self, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, - Size, Vector, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -181,7 +181,7 @@ pub fn draw<Renderer, Handle>( let render = |renderer: &mut Renderer| { renderer.draw_image( - core::Image { + image::Image { handle: handle.clone(), filter_method, rotation: rotation.radians(), diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 4551bcad..bec0090f 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -211,11 +211,13 @@ where let render = |renderer: &mut Renderer| { renderer.draw_svg( - self.handle.clone(), - style.color, + svg::Svg { + handle: self.handle.clone(), + color: style.color, + rotation: self.rotation.radians(), + opacity: self.opacity, + }, drawing_bounds, - self.rotation.radians(), - self.opacity, ); }; From 2b1b9c984ac1b290c351d0a9edc7bca69f8bd526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 05:03:48 +0200 Subject: [PATCH 186/657] Implement missing `draw_svg` in `Frame` wrapper --- graphics/src/geometry/frame.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index f3c0817c..b5f2f139 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -75,12 +75,18 @@ where self.raw.fill_text(text); } - /// Draws the given image on the [`Frame`] inside the given bounds. + /// Draws the given [`Image`] on the [`Frame`] inside the given bounds. #[cfg(feature = "image")] pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { self.raw.draw_image(bounds, image); } + /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "svg")] + pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + self.raw.draw_svg(bounds, svg); + } + /// Stores the current transform of the [`Frame`] and executes the given /// drawing operations, restoring the transform afterwards. /// From 5dc668f901032a076c7b9ff46254c0a0b9d3e663 Mon Sep 17 00:00:00 2001 From: Cho Yunjin <kmoon2437@gmail.com> Date: Tue, 16 Jul 2024 17:19:58 +0900 Subject: [PATCH 187/657] update version of "web-sys" to 0.3.69 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index aa2d950e..65c5007e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,7 +175,7 @@ unicode-segmentation = "1.0" url = "2.5" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" -web-sys = "=0.3.67" +web-sys = "0.3.69" web-time = "1.1" wgpu = "0.19" winapi = "0.3" From cc076903dda18f79dbd82238f7a8216bab8c679d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 4 Aug 2024 14:38:42 +0200 Subject: [PATCH 188/657] Invert Earth's rotation in `solar_system` example --- examples/solar_system/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9da9fd34..1e74f2bd 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -187,7 +187,7 @@ impl<Message> canvas::Program<Message> for State { frame.draw_image( Rectangle::with_radius(Self::EARTH_RADIUS), - canvas::Image::new(&self.earth).rotation(rotation * 20.0), + canvas::Image::new(&self.earth).rotation(-rotation * 20.0), ); frame.rotate(rotation * 10.0); From 3a3fda83cd15506ee7bb629ae44bd2eab203bb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 5 Aug 2024 23:12:26 +0200 Subject: [PATCH 189/657] Implement `State::options` for `combo_box` --- widget/src/combo_box.rs | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 0a4624cb..62785b2c 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -208,12 +208,14 @@ where /// The local state of a [`ComboBox`]. #[derive(Debug, Clone)] -pub struct State<T>(RefCell<Inner<T>>); +pub struct State<T> { + options: Vec<T>, + inner: RefCell<Inner<T>>, +} #[derive(Debug, Clone)] struct Inner<T> { value: String, - options: Vec<T>, option_matchers: Vec<String>, filtered_options: Filtered<T>, } @@ -247,34 +249,44 @@ where .collect(), ); - Self(RefCell::new(Inner { - value, + Self { options, - option_matchers, - filtered_options, - })) + inner: RefCell::new(Inner { + value, + option_matchers, + filtered_options, + }), + } + } + + /// Returns the options of the [`State`]. + /// + /// These are the options provided when the [`State`] + /// was constructed with [`State::new`]. + pub fn options(&self) -> &[T] { + &self.options } fn value(&self) -> String { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); inner.value.clone() } fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); f(&inner) } fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) { - let mut inner = self.0.borrow_mut(); + let mut inner = self.inner.borrow_mut(); f(&mut inner); } fn sync_filtered_options(&self, options: &mut Filtered<T>) { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); inner.filtered_options.sync(options); } @@ -440,7 +452,7 @@ where state.filtered_options.update( search( - &state.options, + &self.state.options, &state.option_matchers, &state.value, ) @@ -589,7 +601,7 @@ where if let Some(selection) = menu.new_selection.take() { // Clear the value and reset the options and menu state.value = String::new(); - state.filtered_options.update(state.options.clone()); + state.filtered_options.update(self.state.options.clone()); menu.menu = menu::State::default(); // Notify the selection From ff0da4dc819a0cb3037502fd2ee82609f25f2fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 5 Aug 2024 23:27:40 +0200 Subject: [PATCH 190/657] Fix `hover` widget not relaying events when overlay is active --- widget/src/helpers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 0eb5f974..11dddfa8 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -523,6 +523,7 @@ where | mouse::Event::ButtonReleased(_) ) ) || cursor.is_over(layout.bounds()) + || self.is_top_overlay_active { let (top_layout, top_tree) = children.next().unwrap(); From 6fbbc30f5cb1f699c61e064ed54fe428d96be7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 6 Aug 2024 03:19:27 +0200 Subject: [PATCH 191/657] Implement `row::Wrapping` widget If you have a `Row`, simply call `Row::wrap` at the end to turn it into a `Row` that will wrap its contents. The original alignment of the `Row` is preserved per row wrapped. --- core/src/layout/node.rs | 13 +-- examples/layout/src/main.rs | 7 +- widget/src/row.rs | 200 ++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 9 deletions(-) diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 5743a9bd..0c0f90fb 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -103,12 +103,13 @@ impl Node { } /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: impl Into<Vector>) -> Self { - let translation = translation.into(); + pub fn translate(mut self, translation: impl Into<Vector>) -> Self { + self.translate_mut(translation); + self + } - Self { - bounds: self.bounds + translation, - ..self - } + /// Translates the [`Node`] by the given translation. + pub fn translate_mut(&mut self, translation: impl Into<Vector>) { + self.bounds = self.bounds + translation.into(); } } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index f39e24f9..cb33369b 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -258,9 +258,10 @@ fn application<'a>() -> Element<'a, Message> { scrollable( column![ "Content!", - square(400), - square(200), - square(400), + row((1..10).map(|i| square(if i % 2 == 0 { 80 } else { 160 }))) + .spacing(20) + .align_y(Center) + .wrap(), "The end" ] .spacing(40) diff --git a/widget/src/row.rs b/widget/src/row.rs index 3feeaa7e..fee2218a 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -142,6 +142,13 @@ where ) -> Self { children.into_iter().fold(self, Self::push) } + + /// Turns the [`Row`] into a [`Wrapping`] row. + /// + /// The original alignment of the [`Row`] is preserved per row wrapped. + pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> { + Wrapping { row: self } + } } impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> @@ -339,3 +346,196 @@ where Self::new(row) } } + +/// A [`Row`] that wraps its contents. +/// +/// Create a [`Row`] first, and then call [`Row::wrap`] to +/// obtain a [`Row`] that wraps its contents. +/// +/// The original alignment of the [`Row`] is preserved per row wrapped. +#[allow(missing_debug_implementations)] +pub struct Wrapping< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> { + row: Row<'a, Message, Theme, Renderer>, +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Wrapping<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.row.children() + } + + fn diff(&self, tree: &mut Tree) { + self.row.diff(tree); + } + + fn size(&self) -> Size<Length> { + self.row.size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .width(self.row.width) + .height(self.row.height) + .shrink(self.row.padding); + + let spacing = self.row.spacing; + let max_width = limits.max().width; + + let mut children: Vec<layout::Node> = Vec::new(); + let mut intrinsic_size = Size::ZERO; + let mut row_start = 0; + let mut row_height = 0.0; + let mut x = 0.0; + let mut y = 0.0; + + let align_factor = match self.row.align { + Alignment::Start => 0.0, + Alignment::Center => 2.0, + Alignment::End => 1.0, + }; + + let align = |row_start: std::ops::Range<usize>, + row_height: f32, + children: &mut Vec<layout::Node>| { + if align_factor != 0.0 { + for node in &mut children[row_start] { + let height = node.size().height; + + node.translate_mut(Vector::new( + 0.0, + (row_height - height) / align_factor, + )); + } + } + }; + + for (i, child) in self.row.children.iter().enumerate() { + let node = child.as_widget().layout( + &mut tree.children[i], + renderer, + &limits, + ); + + let child_size = node.size(); + + if x != 0.0 && x + child_size.width > max_width { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + + align(row_start..i, row_height, &mut children); + + y += row_height + spacing; + x = 0.0; + row_start = i; + row_height = 0.0; + } + + row_height = row_height.max(child_size.height); + + children.push(node.move_to(( + x + self.row.padding.left, + y + self.row.padding.top, + ))); + + x += child_size.width + spacing; + } + + if x != 0.0 { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + } + + intrinsic_size.height = (y - spacing).max(0.0) + row_height; + align(row_start..children.len(), row_height, &mut children); + + let size = + limits.resolve(self.row.width, self.row.height, intrinsic_size); + + layout::Node::with_children(size.expand(self.row.padding), children) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<()>, + ) { + self.row.operate(tree, layout, renderer, operation); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.row.on_event( + tree, event, layout, cursor, renderer, clipboard, shell, viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.row + .mouse_interaction(tree, layout, cursor, viewport, renderer) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.row + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + self.row.overlay(tree, layout, renderer, translation) + } +} + +impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self { + Self::new(row) + } +} From 422568dee49fa6b814ae0131a3f88d4ae2be243b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 8 Aug 2024 01:25:00 +0200 Subject: [PATCH 192/657] Introduce `black_box` and `chain` in `widget::operation` --- core/src/element.rs | 4 +- core/src/overlay.rs | 2 +- core/src/overlay/element.rs | 4 +- core/src/overlay/group.rs | 2 +- core/src/widget.rs | 2 +- core/src/widget/operation.rs | 191 +++++++++++++++++++++++-- core/src/widget/operation/focusable.rs | 30 ++-- examples/toast/src/main.rs | 4 +- runtime/src/lib.rs | 4 +- runtime/src/multi_window/state.rs | 2 +- runtime/src/overlay/nested.rs | 4 +- runtime/src/program/state.rs | 2 +- runtime/src/user_interface.rs | 2 +- widget/src/button.rs | 2 +- widget/src/column.rs | 2 +- widget/src/container.rs | 2 +- widget/src/helpers.rs | 4 +- widget/src/keyed/column.rs | 2 +- widget/src/lazy.rs | 2 +- widget/src/lazy/component.rs | 6 +- widget/src/lazy/responsive.rs | 2 +- widget/src/mouse_area.rs | 2 +- widget/src/pane_grid.rs | 2 +- widget/src/pane_grid/content.rs | 2 +- widget/src/pane_grid/title_bar.rs | 2 +- widget/src/row.rs | 4 +- widget/src/scrollable.rs | 2 +- widget/src/stack.rs | 2 +- widget/src/text_editor.rs | 2 +- widget/src/text_input.rs | 2 +- widget/src/themer.rs | 4 +- 31 files changed, 232 insertions(+), 67 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index 385d8295..6ebb8a15 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -304,7 +304,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.widget.operate(tree, layout, renderer, operation); } @@ -440,7 +440,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.element .widget diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 3b79970e..f09de831 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -41,7 +41,7 @@ where &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation<()>, + _operation: &mut dyn widget::Operation, ) { } diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 61e75e8a..32e987a3 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -92,7 +92,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.overlay.operate(layout, renderer, operation); } @@ -144,7 +144,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.content.operate(layout, renderer, operation); } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index cd12eac9..6541d311 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -132,7 +132,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( diff --git a/core/src/widget.rs b/core/src/widget.rs index 08cfa55b..c5beea54 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -105,7 +105,7 @@ where _state: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn Operation<()>, + _operation: &mut dyn Operation, ) { } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 3e4ed618..741e1a5f 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -12,11 +12,12 @@ use crate::{Rectangle, Vector}; use std::any::Any; use std::fmt; +use std::marker::PhantomData; use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. -pub trait Operation<T>: Send { +pub trait Operation<T = ()>: Send { /// Operates on a widget that contains other widgets. /// /// The `operate_on_children` function can be called to return control to @@ -53,6 +54,46 @@ pub trait Operation<T>: Send { } } +impl<T, O> Operation<O> for Box<T> +where + T: Operation<O> + ?Sized, +{ + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>), + ) { + self.as_mut().container(id, bounds, operate_on_children); + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + self.as_mut().focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + bounds: Rectangle, + translation: Vector, + ) { + self.as_mut().scrollable(state, id, bounds, translation); + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + self.as_mut().text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.as_mut().custom(state, id); + } + + fn finish(&self) -> Outcome<O> { + self.as_ref().finish() + } +} + /// The result of an [`Operation`]. pub enum Outcome<T> { /// The [`Operation`] produced no result. @@ -78,9 +119,62 @@ where } } +/// Wraps the [`Operation`] in a black box, erasing its returning type. +pub fn black_box<'a, T, O>( + operation: &'a mut dyn Operation<T>, +) -> impl Operation<O> + 'a +where + T: 'a, +{ + struct BlackBox<'a, T> { + operation: &'a mut dyn Operation<T>, + } + + impl<'a, T, O> Operation<O> for BlackBox<'a, T> { + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>), + ) { + self.operation.container(id, bounds, &mut |operation| { + operate_on_children(&mut BlackBox { operation }); + }); + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + bounds: Rectangle, + translation: Vector, + ) { + self.operation.scrollable(state, id, bounds, translation); + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + self.operation.text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) { + self.operation.custom(state, id); + } + + fn finish(&self) -> Outcome<O> { + Outcome::None + } + } + + BlackBox { operation } +} + /// Maps the output of an [`Operation`] using the given function. pub fn map<A, B>( - operation: Box<dyn Operation<A>>, + operation: impl Operation<A>, f: impl Fn(A) -> B + Send + Sync + 'static, ) -> impl Operation<B> where @@ -88,13 +182,14 @@ where B: 'static, { #[allow(missing_debug_implementations)] - struct Map<A, B> { - operation: Box<dyn Operation<A>>, + struct Map<O, A, B> { + operation: O, f: Arc<dyn Fn(A) -> B + Send + Sync>, } - impl<A, B> Operation<B> for Map<A, B> + impl<O, A, B> Operation<B> for Map<O, A, B> where + O: Operation<A>, A: 'static, B: 'static, { @@ -155,10 +250,7 @@ where let Self { operation, .. } = self; - MapRef { - operation: operation.as_mut(), - } - .container(id, bounds, operate_on_children); + MapRef { operation }.container(id, bounds, operate_on_children); } fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { @@ -201,6 +293,87 @@ where } } +/// Chains the output of an [`Operation`] with the provided function to +/// build a new [`Operation`]. +pub fn chain<A, B, O>( + operation: impl Operation<A> + 'static, + f: fn(A) -> O, +) -> impl Operation<B> +where + A: 'static, + B: Send + 'static, + O: Operation<B> + 'static, +{ + struct Chain<T, O, A, B> + where + T: Operation<A>, + O: Operation<B>, + { + operation: T, + next: fn(A) -> O, + _result: PhantomData<B>, + } + + impl<T, O, A, B> Operation<B> for Chain<T, O, A, B> + where + T: Operation<A> + 'static, + O: Operation<B> + 'static, + A: 'static, + B: Send + 'static, + { + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + self.operation.container(id, bounds, &mut |operation| { + operate_on_children(&mut black_box(operation)); + }); + } + + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + bounds: Rectangle, + translation: crate::Vector, + ) { + self.operation.scrollable(state, id, bounds, translation); + } + + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + self.operation.text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { + self.operation.custom(state, id); + } + + fn finish(&self) -> Outcome<B> { + match self.operation.finish() { + Outcome::None => Outcome::None, + Outcome::Some(value) => { + Outcome::Chain(Box::new((self.next)(value))) + } + Outcome::Chain(operation) => { + Outcome::Chain(Box::new(chain(operation, self.next))) + } + } + } + } + + Chain { + operation, + next: f, + _result: PhantomData, + } +} + /// Produces an [`Operation`] that applies the given [`Operation`] to the /// children of a container with the given [`Id`]. pub fn scope<T: 'static>( diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 68c22faa..0a6f2e96 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -1,5 +1,5 @@ //! Operate on widgets that can be focused. -use crate::widget::operation::{Operation, Outcome}; +use crate::widget::operation::{self, Operation, Outcome}; use crate::widget::Id; use crate::Rectangle; @@ -58,19 +58,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> { /// Produces an [`Operation`] that generates a [`Count`] and chains it with the /// provided function to build a new [`Operation`]. -pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T> -where - O: Operation<T> + 'static, -{ - struct CountFocusable<O> { +pub fn count() -> impl Operation<Count> { + struct CountFocusable { count: Count, - next: fn(Count) -> O, } - impl<T, O> Operation<T> for CountFocusable<O> - where - O: Operation<T> + 'static, - { + impl Operation<Count> for CountFocusable { fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { if state.is_focused() { self.count.focused = Some(self.count.total); @@ -83,26 +76,25 @@ where &mut self, _id: Option<&Id>, _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + operate_on_children: &mut dyn FnMut(&mut dyn Operation<Count>), ) { operate_on_children(self); } - fn finish(&self) -> Outcome<T> { - Outcome::Chain(Box::new((self.next)(self.count))) + fn finish(&self) -> Outcome<Count> { + Outcome::Some(self.count) } } CountFocusable { count: Count::default(), - next: f, } } /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the previous focusable widget. /// - if not found, focuses the last focusable widget. -pub fn focus_previous<T>() -> impl Operation<T> { +pub fn focus_previous() -> impl Operation { struct FocusPrevious { count: Count, current: usize, @@ -136,13 +128,13 @@ pub fn focus_previous<T>() -> impl Operation<T> { } } - count(|count| FocusPrevious { count, current: 0 }) + operation::chain(count(), |count| FocusPrevious { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the next focusable widget. /// - if not found, focuses the first focusable widget. -pub fn focus_next<T>() -> impl Operation<T> { +pub fn focus_next() -> impl Operation { struct FocusNext { count: Count, current: usize, @@ -170,7 +162,7 @@ pub fn focus_next<T>() -> impl Operation<T> { } } - count(|count| FocusNext { count, current: 0 }) + operation::chain(count(), |count| FocusNext { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 040c19bd..8f6a836e 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -347,7 +347,7 @@ mod toast { state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -589,7 +589,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.toasts diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f27657d1..7230fc73 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -59,7 +59,7 @@ pub enum Action<T> { }, /// Run a widget operation. - Widget(Box<dyn widget::Operation<()>>), + Widget(Box<dyn widget::Operation>), /// Run a clipboard action. Clipboard(clipboard::Action), @@ -79,7 +79,7 @@ pub enum Action<T> { impl<T> Action<T> { /// Creates a new [`Action::Widget`] with the given [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation<()> + 'static) -> Self { + pub fn widget(operation: impl widget::Operation + 'static) -> Self { Self::Widget(Box::new(operation)) } diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 72ce6933..0bec555f 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -205,7 +205,7 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator<Item = Box<dyn Operation<()>>>, + operations: impl Iterator<Item = Box<dyn Operation>>, bounds: Size, debug: &mut Debug, ) { diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 11eee41c..da3e6929 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -131,13 +131,13 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { fn recurse<Message, Theme, Renderer>( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) where Renderer: renderer::Renderer, { diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index e51ad0cb..c377814a 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -178,7 +178,7 @@ where pub fn operate( &mut self, renderer: &mut P::Renderer, - operations: impl Iterator<Item = Box<dyn Operation<()>>>, + operations: impl Iterator<Item = Box<dyn Operation>>, bounds: Size, debug: &mut Debug, ) { diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 858b1a2d..11ebb381 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -566,7 +566,7 @@ where pub fn operate( &mut self, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.root.as_widget().operate( &mut self.state, diff --git a/widget/src/button.rs b/widget/src/button.rs index 64a639d2..eafa71b9 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -236,7 +236,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( diff --git a/widget/src/column.rs b/widget/src/column.rs index ae82ccaa..d3ea4cf7 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -222,7 +222,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/container.rs b/widget/src/container.rs index 9224f2ce..54043ad0 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -245,7 +245,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container( self.id.as_ref().map(|id| &id.0), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 11dddfa8..6bf96c9e 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -289,7 +289,7 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<()>, + operation: &mut dyn operation::Operation, ) { self.content .as_widget() @@ -491,7 +491,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<()>, + operation: &mut dyn operation::Operation, ) { let children = [&self.base, &self.top] .into_iter() diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 69991d1f..2c56c605 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -265,7 +265,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 606da22d..4bcf8628 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -182,7 +182,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.with_element(|element| { element.as_widget().operate( diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index f079c0df..1bf04195 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -59,7 +59,7 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { fn operate( &self, _state: &mut Self::State, - _operation: &mut dyn widget::Operation<()>, + _operation: &mut dyn widget::Operation, ) { } @@ -172,7 +172,7 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); @@ -358,7 +358,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { self.rebuild_element_with_operation(operation); diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 27f52617..2e24f2b3 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -161,7 +161,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::<State>(); let mut content = self.content.borrow_mut(); diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 17cae53b..366335f4 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -178,7 +178,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content.as_widget().operate( &mut tree.children[0], diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index c3da3879..0aab1ab5 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -324,7 +324,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index d45fc0cd..ec0676b1 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -214,7 +214,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index c05f1252..791fab4a 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -278,7 +278,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let mut children = layout.children(); let padded = children.next().unwrap(); diff --git a/widget/src/row.rs b/widget/src/row.rs index fee2218a..85af912f 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -218,7 +218,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -470,7 +470,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.row.operate(tree, layout, renderer, operation); } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 9ba8c39b..cf504eda 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -415,7 +415,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::<State>(); diff --git a/widget/src/stack.rs b/widget/src/stack.rs index efa9711d..001376ac 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -189,7 +189,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a264ba06..aac47b2d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -885,7 +885,7 @@ where tree: &mut widget::Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn widget::Operation<()>, + operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::<State<Highlighter>>(); diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 20e80ba5..173de136 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -542,7 +542,7 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 9eb47d84..499a9fe8 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -104,7 +104,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content .as_widget() @@ -236,7 +236,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { self.content.operate(layout, renderer, operation); } From 0ce81a0e0e237a162dec3c1ed90c4675192d4040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 8 Aug 2024 01:25:15 +0200 Subject: [PATCH 193/657] Display top contents in `hover` widget when focused --- widget/src/helpers.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 6bf96c9e..c3ffea45 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -4,7 +4,8 @@ use crate::checkbox::{self, Checkbox}; use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; -use crate::core::widget::operation; +use crate::core::widget::operation::{self, Operation}; +use crate::core::window; use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; @@ -397,6 +398,7 @@ where struct Hover<'a, Message, Theme, Renderer> { base: Element<'a, Message, Theme, Renderer>, top: Element<'a, Message, Theme, Renderer>, + is_top_focused: bool, is_top_overlay_active: bool, } @@ -472,7 +474,9 @@ where viewport, ); - if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + if cursor.is_over(layout.bounds()) + || self.is_top_focused + || self.is_top_overlay_active { let (top_layout, top_tree) = children.next().unwrap(); @@ -515,6 +519,24 @@ where ) -> event::Status { let mut children = layout.children().zip(&mut tree.children); let (base_layout, base_tree) = children.next().unwrap(); + let (top_layout, top_tree) = children.next().unwrap(); + + if matches!(event, Event::Window(window::Event::RedrawRequested(_))) + { + let mut count_focused = operation::focusable::count(); + + self.top.as_widget_mut().operate( + top_tree, + top_layout, + renderer, + &mut operation::black_box(&mut count_focused), + ); + + self.is_top_focused = match count_focused.finish() { + operation::Outcome::Some(count) => count.focused.is_some(), + _ => false, + }; + } let top_status = if matches!( event, @@ -523,10 +545,9 @@ where | mouse::Event::ButtonReleased(_) ) ) || cursor.is_over(layout.bounds()) + || self.is_top_focused || self.is_top_overlay_active { - let (top_layout, top_tree) = children.next().unwrap(); - self.top.as_widget_mut().on_event( top_tree, event.clone(), @@ -612,6 +633,7 @@ where Element::new(Hover { base: base.into(), top: top.into(), + is_top_focused: false, is_top_overlay_active: false, }) } From f92e01e913480e1450696f3d37af4bff09f661d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maja=20K=C4=85dzio=C5=82ka?= <maya@compilercrim.es> Date: Sun, 11 Aug 2024 22:33:17 +0200 Subject: [PATCH 194/657] iced_winit: drop Clipboard before Window Fixes #2482, avoids nasal daemons --- winit/src/clipboard.rs | 31 +++++++++++++++++++++++-------- winit/src/program.rs | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 5237ca01..f8b90777 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,6 +1,8 @@ //! Access the clipboard. use crate::core::clipboard::Kind; +use winit::window::Window; +use std::sync::Arc; /// A buffer for short-term storage and transfer within and between /// applications. @@ -10,18 +12,31 @@ pub struct Clipboard { } enum State { - Connected(window_clipboard::Clipboard), + Connected { + clipboard: window_clipboard::Clipboard, + // Held until drop to satisfy the safety invariants of + // `window_clipboard::Clipboard`. + // + // Note that the field ordering is load-bearing. + #[allow(dead_code)] + window: Arc<Window>, + }, Unavailable, } impl Clipboard { /// Creates a new [`Clipboard`] for the given window. - pub fn connect(window: &winit::window::Window) -> Clipboard { + pub fn connect(window: Arc<Window>) -> Clipboard { #[allow(unsafe_code)] - let state = unsafe { window_clipboard::Clipboard::connect(window) } - .ok() - .map(State::Connected) - .unwrap_or(State::Unavailable); + // SAFETY: The window handle will stay alive throughout the entire + // lifetime of the `window_clipboard::Clipboard` because we hold + // the `Arc<Window>` together with `State`, and enum variant fields + // get dropped in declaration order. + let clipboard = unsafe { window_clipboard::Clipboard::connect(&window) }; + let state = match clipboard { + Ok(clipboard) => State::Connected { clipboard, window }, + Err(_) => State::Unavailable, + }; Clipboard { state } } @@ -37,7 +52,7 @@ impl Clipboard { /// Reads the current content of the [`Clipboard`] as text. pub fn read(&self, kind: Kind) -> Option<String> { match &self.state { - State::Connected(clipboard) => match kind { + State::Connected { clipboard, .. } => match kind { Kind::Standard => clipboard.read().ok(), Kind::Primary => clipboard.read_primary().and_then(Result::ok), }, @@ -48,7 +63,7 @@ impl Clipboard { /// Writes the given text contents to the [`Clipboard`]. pub fn write(&mut self, kind: Kind, contents: String) { match &mut self.state { - State::Connected(clipboard) => { + State::Connected { clipboard, .. } => { let result = match kind { Kind::Standard => clipboard.write(contents), Kind::Primary => { diff --git a/winit/src/program.rs b/winit/src/program.rs index 3d709b7e..139b2b8f 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -307,7 +307,7 @@ where } }; - let clipboard = Clipboard::connect(&window); + let clipboard = Clipboard::connect(window.clone()); let finish_boot = async move { let mut compositor = From 03472dfd4f8a472f38f03d332b4835580eb84489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 02:53:23 +0200 Subject: [PATCH 195/657] Make `Padding` affect `text_editor` clipping --- core/src/rectangle.rs | 26 ++++++++++++++++++++------ widget/src/text_editor.rs | 27 ++++++++++----------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 1556e072..155cfcbf 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,4 +1,4 @@ -use crate::{Point, Radians, Size, Vector}; +use crate::{Padding, Point, Radians, Size, Vector}; /// An axis-aligned rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -164,12 +164,26 @@ impl Rectangle<f32> { } /// Expands the [`Rectangle`] a given amount. - pub fn expand(self, amount: f32) -> Self { + pub fn expand(self, padding: impl Into<Padding>) -> Self { + let padding = padding.into(); + Self { - x: self.x - amount, - y: self.y - amount, - width: self.width + amount * 2.0, - height: self.height + amount * 2.0, + x: self.x - padding.left, + y: self.y - padding.top, + width: self.width + padding.horizontal(), + height: self.height + padding.vertical(), + } + } + + /// Shrinks the [`Rectangle`] a given amount. + pub fn shrink(self, padding: impl Into<Padding>) -> Self { + let padding = padding.into(); + + Self { + x: self.x + padding.left, + y: self.y + padding.top, + width: self.width - padding.horizontal(), + height: self.height - padding.vertical(), } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 8b4b892d..9e8479fc 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -768,20 +768,14 @@ where style.background, ); - let position = bounds.position() - + Vector::new(self.padding.left, self.padding.top); + let text_bounds = bounds.shrink(self.padding); if internal.editor.is_empty() { if let Some(placeholder) = self.placeholder.clone() { renderer.fill_text( Text { content: placeholder.into_owned(), - bounds: bounds.size() - - Size::new( - self.padding.right, - self.padding.bottom, - ), - + bounds: text_bounds.size(), size: self .text_size .unwrap_or_else(|| renderer.default_size()), @@ -791,7 +785,7 @@ where vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, }, - position, + text_bounds.position(), style.placeholder, bounds, ); @@ -799,16 +793,13 @@ where } else { renderer.fill_editor( &internal.editor, - position, + text_bounds.position(), defaults.text_color, - bounds, + text_bounds, ); } - let translation = Vector::new( - bounds.x + self.padding.left, - bounds.y + self.padding.top, - ); + let translation = text_bounds.position() - Point::ORIGIN; if let Some(focus) = state.focus.as_ref() { match internal.editor.cursor() { @@ -826,7 +817,9 @@ where ), ); - if let Some(clipped_cursor) = bounds.intersection(&cursor) { + if let Some(clipped_cursor) = + text_bounds.intersection(&cursor) + { renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -843,7 +836,7 @@ where } Cursor::Selection(ranges) => { for range in ranges.into_iter().filter_map(|range| { - bounds.intersection(&(range + translation)) + text_bounds.intersection(&(range + translation)) }) { renderer.fill_quad( renderer::Quad { From be7d175388076cf24ca902a2d4cd457ce2a8e9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 02:54:22 +0200 Subject: [PATCH 196/657] Remove cursor snapping hack in `text_editor` The `quad` shader now properly takes care of snapping lines to the pixel grid. --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 9e8479fc..32537aff 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -823,7 +823,7 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: clipped_cursor.x.floor(), + x: clipped_cursor.x, y: clipped_cursor.y, width: clipped_cursor.width, height: clipped_cursor.height, From 373e887a5834dbf2272aeaea8f1820ee90698400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 02:55:49 +0200 Subject: [PATCH 197/657] Focus `text_editor` at start-up in `editor` example --- examples/editor/src/main.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 155e74a1..aa07b328 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,7 +1,7 @@ use iced::highlighter; use iced::keyboard; use iced::widget::{ - button, column, container, horizontal_space, pick_list, row, text, + self, button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; use iced::{Center, Element, Fill, Font, Subscription, Task, Theme}; @@ -49,13 +49,16 @@ impl Editor { is_loading: true, is_dirty: false, }, - Task::perform( - load_file(format!( - "{}/src/main.rs", - env!("CARGO_MANIFEST_DIR") - )), - Message::FileOpened, - ), + Task::batch([ + Task::perform( + load_file(format!( + "{}/src/main.rs", + env!("CARGO_MANIFEST_DIR") + )), + Message::FileOpened, + ), + widget::focus_next(), + ]), ) } From 3e59d824f8be029720f4064b49099e6aabc11179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 02:57:45 +0200 Subject: [PATCH 198/657] Fix clipping area of `text_editor` placeholder --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 32537aff..e41c50d7 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -787,7 +787,7 @@ where }, text_bounds.position(), style.placeholder, - bounds, + text_bounds, ); } } else { From 7decbb3d5d0e72fd4667840568411bcb867feca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 03:07:11 +0200 Subject: [PATCH 199/657] Fix formatting in `iced_winit::clipboard` --- winit/src/clipboard.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index f8b90777..7ae646fc 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,8 +1,8 @@ //! Access the clipboard. use crate::core::clipboard::Kind; -use winit::window::Window; use std::sync::Arc; +use winit::window::Window; /// A buffer for short-term storage and transfer within and between /// applications. @@ -27,12 +27,14 @@ enum State { impl Clipboard { /// Creates a new [`Clipboard`] for the given window. pub fn connect(window: Arc<Window>) -> Clipboard { - #[allow(unsafe_code)] // SAFETY: The window handle will stay alive throughout the entire // lifetime of the `window_clipboard::Clipboard` because we hold // the `Arc<Window>` together with `State`, and enum variant fields // get dropped in declaration order. - let clipboard = unsafe { window_clipboard::Clipboard::connect(&window) }; + #[allow(unsafe_code)] + let clipboard = + unsafe { window_clipboard::Clipboard::connect(&window) }; + let state = match clipboard { Ok(clipboard) => State::Connected { clipboard, window }, Err(_) => State::Unavailable, From afa8ad3b11818c431ea8c40fc4af10de528d9a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 03:10:46 +0200 Subject: [PATCH 200/657] Fix `integration` example --- examples/integration/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 9818adf3..5b64cbd1 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -68,7 +68,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { Size::new(physical_size.width, physical_size.height), window.scale_factor(), ); - let clipboard = Clipboard::connect(&window); + let clipboard = Clipboard::connect(window.clone()); let backend = wgpu::util::backend_bits_from_env().unwrap_or_default(); From 7740c35a2a0f162b04f78075afa5a8e2448a782c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 03:25:28 +0200 Subject: [PATCH 201/657] Use `clipped_cursor` directly in `text_editor` --- widget/src/text_editor.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 85332ba4..745e3ae8 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -822,12 +822,7 @@ where { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: clipped_cursor.x, - y: clipped_cursor.y, - width: clipped_cursor.width, - height: clipped_cursor.height, - }, + bounds: clipped_cursor, ..renderer::Quad::default() }, style.value, From 01aa84e41afa556fd4e82ef11f2f55cf443ef1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 05:12:42 +0200 Subject: [PATCH 202/657] Make `window::close` return and introduce `Task::discard` --- examples/events/src/main.rs | 6 ++++-- examples/exit/src/main.rs | 4 +++- runtime/src/task.rs | 11 +++++++++++ runtime/src/window.rs | 6 +++--- winit/src/program.rs | 4 +++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 5bada9b5..e432eb14 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -37,7 +37,7 @@ impl Events { } Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { - window::get_latest().and_then(window::close) + window::get_latest().and_then(window::close).discard() } else { Task::none() } @@ -47,7 +47,9 @@ impl Events { Task::none() } - Message::Exit => window::get_latest().and_then(window::close), + Message::Exit => { + window::get_latest().and_then(window::close).discard() + } } } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 48b0864c..d8334bcc 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -20,7 +20,9 @@ enum Message { impl Exit { fn update(&mut self, message: Message) -> Task<Message> { match message { - Message::Confirm => window::get_latest().and_then(window::close), + Message::Confirm => { + window::get_latest().and_then(window::close).discard() + } Message::Exit => { self.show_confirm = true; diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 4d75ddaa..ec8d7cc7 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -159,6 +159,17 @@ impl<T> Task<T> { } } + /// Creates a new [`Task`] that discards the result of the current one. + /// + /// Useful if you only care about the side effects of a [`Task`]. + pub fn discard<O>(self) -> Task<O> + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + self.then(|_| Task::none()) + } + /// Creates a new [`Task`] that can be aborted with the returned [`Handle`]. pub fn abortable(self) -> (Self, Handle) where diff --git a/runtime/src/window.rs b/runtime/src/window.rs index cd27cdfe..0d280f1f 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -24,7 +24,7 @@ pub enum Action { Open(Id, Settings, oneshot::Sender<Id>), /// Close the window and exits the application. - Close(Id), + Close(Id, oneshot::Sender<Id>), /// Gets the [`Id`] of the oldest window. GetOldest(oneshot::Sender<Option<Id>>), @@ -230,8 +230,8 @@ pub fn open(settings: Settings) -> (Id, Task<Id>) { } /// Closes the window with `id`. -pub fn close<T>(id: Id) -> Task<T> { - task::effect(crate::Action::Window(Action::Close(id))) +pub fn close(id: Id) -> Task<Id> { + task::oneshot(|channel| crate::Action::Window(Action::Close(id, channel))) } /// Gets the window [`Id`] of the oldest window. diff --git a/winit/src/program.rs b/winit/src/program.rs index 139b2b8f..a51f4fd7 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1209,9 +1209,11 @@ fn run_action<P, C>( *is_window_opening = true; } - window::Action::Close(id) => { + window::Action::Close(id, channel) => { let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); + + let _ = channel.send(id); } window::Action::GetOldest(channel) => { let id = From 22fc5ce0ea83b43cdccea26afd5e545880dfdaf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 05:20:44 +0200 Subject: [PATCH 203/657] Produce `window::Event::Closed` on `window::close` --- winit/src/program.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index a51f4fd7..66f359f4 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -754,6 +754,7 @@ async fn run_instance<P, C>( action, &program, &mut compositor, + &mut events, &mut messages, &mut clipboard, &mut control_sender, @@ -1161,6 +1162,7 @@ fn run_action<P, C>( action: Action<P::Message>, program: &P, compositor: &mut C, + events: &mut Vec<(window::Id, core::Event)>, messages: &mut Vec<P::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender<Control>, @@ -1212,8 +1214,12 @@ fn run_action<P, C>( window::Action::Close(id, channel) => { let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); - let _ = channel.send(id); + + events.push(( + id, + core::Event::Window(core::window::Event::Closed), + )); } window::Action::GetOldest(channel) => { let id = From 8b45d620d048c33febbead4480d9ef62f196c9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Mon, 12 Aug 2024 05:50:22 +0200 Subject: [PATCH 204/657] Revert `window::close` producing a `window::Id` Instead, subscribing to `window::close_events` is preferable; since most use cases will want to react to the user closing a window as well. --- examples/events/src/main.rs | 6 ++---- examples/exit/src/main.rs | 4 +--- runtime/src/window.rs | 6 +++--- winit/src/program.rs | 3 +-- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index e432eb14..5bada9b5 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -37,7 +37,7 @@ impl Events { } Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { - window::get_latest().and_then(window::close).discard() + window::get_latest().and_then(window::close) } else { Task::none() } @@ -47,9 +47,7 @@ impl Events { Task::none() } - Message::Exit => { - window::get_latest().and_then(window::close).discard() - } + Message::Exit => window::get_latest().and_then(window::close), } } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index d8334bcc..48b0864c 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -20,9 +20,7 @@ enum Message { impl Exit { fn update(&mut self, message: Message) -> Task<Message> { match message { - Message::Confirm => { - window::get_latest().and_then(window::close).discard() - } + Message::Confirm => window::get_latest().and_then(window::close), Message::Exit => { self.show_confirm = true; diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 0d280f1f..cd27cdfe 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -24,7 +24,7 @@ pub enum Action { Open(Id, Settings, oneshot::Sender<Id>), /// Close the window and exits the application. - Close(Id, oneshot::Sender<Id>), + Close(Id), /// Gets the [`Id`] of the oldest window. GetOldest(oneshot::Sender<Option<Id>>), @@ -230,8 +230,8 @@ pub fn open(settings: Settings) -> (Id, Task<Id>) { } /// Closes the window with `id`. -pub fn close(id: Id) -> Task<Id> { - task::oneshot(|channel| crate::Action::Window(Action::Close(id, channel))) +pub fn close<T>(id: Id) -> Task<T> { + task::effect(crate::Action::Window(Action::Close(id))) } /// Gets the window [`Id`] of the oldest window. diff --git a/winit/src/program.rs b/winit/src/program.rs index 66f359f4..efe8a978 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1211,10 +1211,9 @@ fn run_action<P, C>( *is_window_opening = true; } - window::Action::Close(id, channel) => { + window::Action::Close(id) => { let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); - let _ = channel.send(id); events.push(( id, From d6ae1fca678060991d1d32ec69beb85616f8312b Mon Sep 17 00:00:00 2001 From: JL710 <76447362+JL710@users.noreply.github.com> Date: Wed, 14 Aug 2024 08:40:22 +0200 Subject: [PATCH 205/657] Remove out of date comment from custom-widget example --- examples/custom_widget/src/main.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index dc3f74ac..58f3c54a 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -1,14 +1,5 @@ //! This example showcases a simple native custom widget that draws a circle. mod circle { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. use iced::advanced::layout::{self, Layout}; use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; From 9ed7fb88663963c3b53eb109eebbc82ebfa479f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 14 Aug 2024 18:02:33 +0200 Subject: [PATCH 206/657] Fix cursor passthrough in `Stack` during `draw` --- examples/modal/src/main.rs | 28 +++++++++------- widget/src/stack.rs | 69 ++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index f1f0e8ad..067ca24d 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -201,19 +201,21 @@ where { stack![ base.into(), - mouse_area(center(opaque(content)).style(|_theme| { - container::Style { - background: Some( - Color { - a: 0.8, - ..Color::BLACK - } - .into(), - ), - ..container::Style::default() - } - })) - .on_press(on_blur) + opaque( + mouse_area(center(opaque(content)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() + } + })) + .on_press(on_blur) + ) ] .into() } diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 001376ac..f220561b 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -269,15 +269,53 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - for (i, ((layer, state), layout)) in self + let layers_below = if cursor == mouse::Cursor::Unavailable { + self.children.len() + } else { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .position(|((layer, state), layout)| { + let interaction = layer.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ); + + interaction != mouse::Interaction::None + }) + .map(|i| self.children.len() - i - 1) + .unwrap_or(self.children.len()) + }; + + let mut layers = self .children .iter() .zip(&tree.children) .zip(layout.children()) - .enumerate() - { - if i > 0 { - renderer.with_layer(clipped_viewport, |renderer| { + .enumerate(); + + let layers = layers.by_ref(); + + let mut draw_layer = + |i, + layer: &Element<'a, Message, Theme, Renderer>, + state, + layout, + cursor| { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { layer.as_widget().draw( state, renderer, @@ -287,18 +325,15 @@ where cursor, &clipped_viewport, ); - }); - } else { - layer.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - &clipped_viewport, - ); - } + } + }; + + for (i, ((layer, state), layout)) in layers.take(layers_below) { + draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable); + } + + for (i, ((layer, state), layout)) in layers { + draw_layer(i, layer, state, layout, cursor); } } } From 5d7d74ffa4c93d562e39ac48f7b11e7266520b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 14 Aug 2024 18:07:26 +0200 Subject: [PATCH 207/657] Find `layers_below` only if `Stack` is hovered --- widget/src/stack.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index f220561b..50e8fabb 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -269,9 +269,7 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - let layers_below = if cursor == mouse::Cursor::Unavailable { - self.children.len() - } else { + let layers_below = if cursor.is_over(layout.bounds()) { self.children .iter() .rev() @@ -286,6 +284,8 @@ where }) .map(|i| self.children.len() - i - 1) .unwrap_or(self.children.len()) + } else { + self.children.len() }; let mut layers = self From 889d8b891fc6c9e74292767d009eb1481086b412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 14 Aug 2024 19:04:26 +0200 Subject: [PATCH 208/657] Fix scroll event passthrough in `Stack` widget --- widget/src/stack.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 50e8fabb..b19212a6 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -209,19 +209,23 @@ where tree: &mut Tree, event: Event, layout: Layout<'_>, - cursor: mouse::Cursor, + mut cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { + let is_over_scroll = + matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. })) + && cursor.is_over(layout.bounds()); + self.children .iter_mut() .rev() .zip(tree.children.iter_mut().rev()) .zip(layout.children().rev()) .map(|((child, state), layout)| { - child.as_widget_mut().on_event( + let status = child.as_widget_mut().on_event( state, event.clone(), layout, @@ -230,7 +234,19 @@ where clipboard, shell, viewport, - ) + ); + + if is_over_scroll { + let interaction = child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ); + + if interaction != mouse::Interaction::None { + cursor = mouse::Cursor::Unavailable; + } + } + + status }) .find(|&status| status == event::Status::Captured) .unwrap_or(event::Status::Ignored) @@ -283,9 +299,9 @@ where interaction != mouse::Interaction::None }) .map(|i| self.children.len() - i - 1) - .unwrap_or(self.children.len()) + .unwrap_or_default() } else { - self.children.len() + 0 }; let mut layers = self From cfd2e7b116e49b295b3bb484a3f477a9f356d493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 14 Aug 2024 19:06:16 +0200 Subject: [PATCH 209/657] Short-circuit scrolling passthrough in `Stack` --- widget/src/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index b19212a6..9ccaa274 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -236,7 +236,7 @@ where viewport, ); - if is_over_scroll { + if is_over_scroll && cursor != mouse::Cursor::Unavailable { let interaction = child.as_widget().mouse_interaction( state, layout, cursor, viewport, renderer, ); From 515772c9f8eeaa318bad52fd04caba4da69b075e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 15 Aug 2024 01:30:24 +0200 Subject: [PATCH 210/657] Rename `operation::chain` to `then` ... and make `focus_*` operations generic over the output type. --- core/src/widget/operation.rs | 4 ++-- core/src/widget/operation/focusable.rs | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 741e1a5f..4ee4b4a7 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -295,7 +295,7 @@ where /// Chains the output of an [`Operation`] with the provided function to /// build a new [`Operation`]. -pub fn chain<A, B, O>( +pub fn then<A, B, O>( operation: impl Operation<A> + 'static, f: fn(A) -> O, ) -> impl Operation<B> @@ -361,7 +361,7 @@ where Outcome::Chain(Box::new((self.next)(value))) } Outcome::Chain(operation) => { - Outcome::Chain(Box::new(chain(operation, self.next))) + Outcome::Chain(Box::new(then(operation, self.next))) } } } diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 0a6f2e96..867c682e 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -94,7 +94,10 @@ pub fn count() -> impl Operation<Count> { /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the previous focusable widget. /// - if not found, focuses the last focusable widget. -pub fn focus_previous() -> impl Operation { +pub fn focus_previous<T>() -> impl Operation<T> +where + T: Send + 'static, +{ struct FocusPrevious { count: Count, current: usize, @@ -128,13 +131,16 @@ pub fn focus_previous() -> impl Operation { } } - operation::chain(count(), |count| FocusPrevious { count, current: 0 }) + operation::then(count(), |count| FocusPrevious { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget, and /// - if found, focuses the next focusable widget. /// - if not found, focuses the first focusable widget. -pub fn focus_next() -> impl Operation { +pub fn focus_next<T>() -> impl Operation<T> +where + T: Send + 'static, +{ struct FocusNext { count: Count, current: usize, @@ -162,7 +168,7 @@ pub fn focus_next() -> impl Operation { } } - operation::chain(count(), |count| FocusNext { count, current: 0 }) + operation::then(count(), |count| FocusNext { count, current: 0 }) } /// Produces an [`Operation`] that searches for the current focused widget From 7c2abc9b8b6898459be56b7b4ae197eadb7dc0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 15 Aug 2024 01:52:45 +0200 Subject: [PATCH 211/657] Fix crash when application boots from a URL event in macOS --- winit/src/program.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index efe8a978..57cef684 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -650,7 +650,7 @@ async fn run_instance<P, C>( mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>, mut proxy: Proxy<P::Message>, mut debug: Debug, - mut boot: oneshot::Receiver<Boot<C>>, + boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, mut control_sender: mpsc::UnboundedSender<Control>, is_daemon: bool, @@ -665,7 +665,7 @@ async fn run_instance<P, C>( let Boot { mut compositor, mut clipboard, - } = boot.try_recv().ok().flatten().expect("Receive boot"); + } = boot.await.expect("Receive boot"); let mut window_manager = WindowManager::new(); let mut is_window_opening = !is_daemon; @@ -679,7 +679,18 @@ async fn run_instance<P, C>( debug.startup_finished(); - while let Some(event) = event_receiver.next().await { + loop { + // Empty the queue if possible + let event = if let Ok(event) = event_receiver.try_next() { + event + } else { + event_receiver.next().await + }; + + let Some(event) = event else { + break; + }; + match event { Event::WindowCreated { id, From 9b99b932bced46047ec2e18c2b6ec5a6c5b3636f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 15 Aug 2024 02:11:17 +0200 Subject: [PATCH 212/657] Produce `window::Event::Closed` only if window exists --- winit/src/program.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 57cef684..c5c3133d 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1223,13 +1223,15 @@ fn run_action<P, C>( *is_window_opening = true; } window::Action::Close(id) => { - let _ = window_manager.remove(id); + let window = window_manager.remove(id); let _ = ui_caches.remove(&id); - events.push(( - id, - core::Event::Window(core::window::Event::Closed), - )); + if window.is_some() { + events.push(( + id, + core::Event::Window(core::window::Event::Closed), + )); + } } window::Action::GetOldest(channel) => { let id = From 6dc71f6f3b1fbf5d0f027e815ccb041cd8e507ae Mon Sep 17 00:00:00 2001 From: Andy Terra <152812+airstrike@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:44:58 -0400 Subject: [PATCH 213/657] Expose additional mouse interaction cursors --- core/src/mouse/interaction.rs | 5 +++++ winit/src/conversion.rs | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index 065eb8e7..92842668 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -13,6 +13,11 @@ pub enum Interaction { Grabbing, ResizingHorizontally, ResizingVertically, + ResizingDiagonalUp, + ResizingDiagonalDown, NotAllowed, ZoomIn, + ZoomOut, + Cell, + Move, } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index e88ff84d..b974b3b9 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -423,8 +423,17 @@ pub fn mouse_interaction( winit::window::CursorIcon::EwResize } Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, + Interaction::ResizingDiagonalUp => { + winit::window::CursorIcon::NeswResize + } + Interaction::ResizingDiagonalDown => { + winit::window::CursorIcon::NwseResize + } Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed, Interaction::ZoomIn => winit::window::CursorIcon::ZoomIn, + Interaction::ZoomOut => winit::window::CursorIcon::ZoomOut, + Interaction::Cell => winit::window::CursorIcon::Cell, + Interaction::Move => winit::window::CursorIcon::Move, } } From 55764b923e69afa8a92b4a6cbd34ee9ddbf8a03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 21 Aug 2024 02:34:03 +0200 Subject: [PATCH 214/657] Decouple `markdown` widget from built-in `Theme` --- widget/src/markdown.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 23e36435..2bcbde5c 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -7,8 +7,8 @@ use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; -use crate::core::theme::{self, Theme}; -use crate::core::{self, color, Color, Element, Length, Pixels}; +use crate::core::theme; +use crate::core::{self, color, Color, Element, Length, Pixels, Theme}; use crate::{column, container, rich_text, row, scrollable, span, text}; pub use pulldown_cmark::HeadingLevel; @@ -349,11 +349,12 @@ impl Default for Settings { /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. -pub fn view<'a, Renderer>( +pub fn view<'a, Theme, Renderer>( items: impl IntoIterator<Item = &'a Item>, settings: Settings, ) -> Element<'a, Url, Theme, Renderer> where + Theme: Catalog + 'a, Renderer: core::text::Renderer<Font = Font> + 'a, { let Settings { @@ -426,9 +427,23 @@ where ) .width(Length::Fill) .padding(spacing.0 / 2.0) - .style(container::dark) + .class(Theme::code_block()) .into(), }); Element::new(column(blocks).width(Length::Fill).spacing(text_size)) } + +/// The theme catalog of Markdown items. +pub trait Catalog: + container::Catalog + scrollable::Catalog + text::Catalog +{ + /// The styling class of a Markdown code block. + fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>; +} + +impl Catalog for Theme { + fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> { + Box::new(container::dark) + } +} From 4c883f12b4761c7e0b273d9a2380552336f61d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 22 Aug 2024 02:24:06 +0200 Subject: [PATCH 215/657] Make `RichText` generic over data structure ... and decouple `markdown::parse` from theming --- core/src/padding.rs | 2 +- core/src/text.rs | 2 +- examples/markdown/src/main.rs | 18 +-- widget/src/container.rs | 1 - widget/src/helpers.rs | 5 +- widget/src/markdown.rs | 254 ++++++++++++++++++++++++++-------- widget/src/text/rich.rs | 35 ++--- 7 files changed, 223 insertions(+), 94 deletions(-) diff --git a/core/src/padding.rs b/core/src/padding.rs index fdaa0236..e26cdd9b 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -32,7 +32,7 @@ use crate::{Pixels, Size}; /// let widget = Widget::new().padding(20); // 20px on all sides /// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right /// ``` -#[derive(Debug, Copy, Clone, Default)] +#[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct Padding { /// Top padding pub top: f32, diff --git a/core/src/text.rs b/core/src/text.rs index 436fee9a..dc8f5785 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -252,7 +252,7 @@ pub struct Span<'a, Link = (), Font = crate::Font> { } /// A text highlight. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Highlight { /// The [`Background`] of the highlight. pub background: Background, diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index eb51f985..5605478f 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -29,8 +29,7 @@ impl Markdown { ( Self { content: text_editor::Content::with_text(INITIAL_CONTENT), - items: markdown::parse(INITIAL_CONTENT, theme.palette()) - .collect(), + items: markdown::parse(INITIAL_CONTENT).collect(), theme, }, widget::focus_next(), @@ -45,11 +44,8 @@ impl Markdown { self.content.perform(action); if is_edit { - self.items = markdown::parse( - &self.content.text(), - self.theme.palette(), - ) - .collect(); + self.items = + markdown::parse(&self.content.text()).collect(); } } Message::LinkClicked(link) => { @@ -67,8 +63,12 @@ impl Markdown { .font(Font::MONOSPACE) .highlight("markdown", highlighter::Theme::Base16Ocean); - let preview = markdown(&self.items, markdown::Settings::default()) - .map(Message::LinkClicked); + let preview = markdown( + &self.items, + markdown::Settings::default(), + markdown::Style::from_palette(self.theme.palette()), + ) + .map(Message::LinkClicked); row![editor, scrollable(preview).spacing(10).height(Fill)] .spacing(10) diff --git a/widget/src/container.rs b/widget/src/container.rs index 54043ad0..ba315741 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -184,7 +184,6 @@ where } /// Sets the style class of the [`Container`]. - #[cfg(feature = "advanced")] #[must_use] pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { self.class = class.into(); diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index c3ffea45..1cb02830 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -25,7 +25,7 @@ use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; use crate::{Column, MouseArea, Row, Space, Stack, Themer}; -use std::borrow::{Borrow, Cow}; +use std::borrow::Borrow; use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. @@ -707,12 +707,13 @@ where /// /// [`Rich`]: text::Rich pub fn rich_text<'a, Link, Theme, Renderer>( - spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>, + spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a, ) -> text::Rich<'a, Link, Theme, Renderer> where Link: Clone + 'static, Theme: text::Catalog + 'a, Renderer: core::text::Renderer, + Renderer::Font: 'a, { text::Rich::with_spans(spans) } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 2bcbde5c..ef4da0df 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -8,9 +8,15 @@ use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; use crate::core::theme; -use crate::core::{self, color, Color, Element, Length, Pixels, Theme}; +use crate::core::{ + self, color, Color, Element, Length, Padding, Pixels, Theme, +}; use crate::{column, container, rich_text, row, scrollable, span, text}; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub use core::text::Highlight; pub use pulldown_cmark::HeadingLevel; pub use url::Url; @@ -18,13 +24,13 @@ pub use url::Url; #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(pulldown_cmark::HeadingLevel, Vec<text::Span<'static, Url>>), + Heading(pulldown_cmark::HeadingLevel, Text), /// A paragraph. - Paragraph(Vec<text::Span<'static, Url>>), + Paragraph(Text), /// A code block. /// /// You can enable the `highlighter` feature for syntax highligting. - CodeBlock(Vec<text::Span<'static, Url>>), + CodeBlock(Text), /// A list. List { /// The first number of the list, if it is ordered. @@ -34,11 +40,112 @@ pub enum Item { }, } +/// A bunch of parsed Markdown text. +#[derive(Debug, Clone)] +pub struct Text { + spans: Vec<Span>, + last_style: Cell<Option<Style>>, + last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>, +} + +impl Text { + fn new(spans: Vec<Span>) -> Self { + Self { + spans, + last_style: Cell::default(), + last_styled_spans: RefCell::default(), + } + } + + /// Returns the [`rich_text`] spans ready to be used for the given style. + /// + /// This method performs caching for you. It will only reallocate if the [`Style`] + /// provided changes. + pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> { + if Some(style) != self.last_style.get() { + *self.last_styled_spans.borrow_mut() = + self.spans.iter().map(|span| span.view(&style)).collect(); + + self.last_style.set(Some(style)); + } + + self.last_styled_spans.borrow().clone() + } +} + +#[derive(Debug, Clone)] +enum Span { + Standard { + text: String, + strikethrough: bool, + link: Option<Url>, + strong: bool, + emphasis: bool, + code: bool, + }, + #[cfg(feature = "highlighter")] + Highlight { + text: String, + color: Option<Color>, + font: Option<Font>, + }, +} + +impl Span { + fn view(&self, style: &Style) -> text::Span<'static, Url> { + match self { + Span::Standard { + text, + strikethrough, + link, + strong, + emphasis, + code, + } => { + let span = span(text.clone()).strikethrough(*strikethrough); + + let span = if *code { + span.font(Font::MONOSPACE) + .color(style.inline_code_color) + .background(style.inline_code_highlight.background) + .border(style.inline_code_highlight.border) + .padding(style.inline_code_padding) + } else if *strong || *emphasis { + span.font(Font { + weight: if *strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if *emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = if let Some(link) = link.as_ref() { + span.color(style.link_color).link(link.clone()) + } else { + span + }; + + span + } + #[cfg(feature = "highlighter")] + Span::Highlight { text, color, font } => { + span(text.clone()).color_maybe(*color).font_maybe(*font) + } + } + } +} + /// Parse the given Markdown content. -pub fn parse( - markdown: &str, - palette: theme::Palette, -) -> impl Iterator<Item = Item> + '_ { +pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ { struct List { start: Option<u64>, items: Vec<Vec<Item>>, @@ -158,7 +265,7 @@ pub fn parse( pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { produce( &mut lists, - Item::Heading(level, spans.drain(..).collect()), + Item::Heading(level, Text::new(spans.drain(..).collect())), ) } pulldown_cmark::TagEnd::Strong if !metadata && !table => { @@ -178,7 +285,10 @@ pub fn parse( None } pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { - produce(&mut lists, Item::Paragraph(spans.drain(..).collect())) + produce( + &mut lists, + Item::Paragraph(Text::new(spans.drain(..).collect())), + ) } pulldown_cmark::TagEnd::Item if !metadata && !table => { if spans.is_empty() { @@ -186,7 +296,7 @@ pub fn parse( } else { produce( &mut lists, - Item::Paragraph(spans.drain(..).collect()), + Item::Paragraph(Text::new(spans.drain(..).collect())), ) } } @@ -207,7 +317,10 @@ pub fn parse( highlighter = None; } - produce(&mut lists, Item::CodeBlock(spans.drain(..).collect())) + produce( + &mut lists, + Item::CodeBlock(Text::new(spans.drain(..).collect())), + ) } pulldown_cmark::TagEnd::MetadataBlock(_) => { metadata = false; @@ -227,9 +340,11 @@ pub fn parse( for (range, highlight) in highlighter.highlight_line(text.as_ref()) { - let span = span(text[range].to_owned()) - .color_maybe(highlight.color()) - .font_maybe(highlight.font()); + let span = Span::Highlight { + text: text[range].to_owned(), + color: highlight.color(), + font: highlight.font(), + }; spans.push(span); } @@ -237,30 +352,13 @@ pub fn parse( return None; } - let span = span(text.into_string()).strikethrough(strikethrough); - - let span = if strong || emphasis { - span.font(Font { - weight: if strong { - font::Weight::Bold - } else { - font::Weight::Normal - }, - style: if emphasis { - font::Style::Italic - } else { - font::Style::Normal - }, - ..Font::default() - }) - } else { - span - }; - - let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) - } else { - span + let span = Span::Standard { + text: text.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: false, }; spans.push(span); @@ -268,29 +366,38 @@ pub fn parse( None } pulldown_cmark::Event::Code(code) if !metadata && !table => { - let span = span(code.into_string()) - .font(Font::MONOSPACE) - .color(Color::WHITE) - .background(color!(0x111111)) - .border(border::rounded(2)) - .padding(padding::left(2).right(2)) - .strikethrough(strikethrough); - - let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) - } else { - span + let span = Span::Standard { + text: code.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: true, }; spans.push(span); None } pulldown_cmark::Event::SoftBreak if !metadata && !table => { - spans.push(span(" ").strikethrough(strikethrough)); + spans.push(Span::Standard { + text: String::from(" "), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); None } pulldown_cmark::Event::HardBreak if !metadata && !table => { - spans.push(span("\n")); + spans.push(Span::Standard { + text: String::from("\n"), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); None } _ => None, @@ -346,12 +453,41 @@ impl Default for Settings { } } +/// The text styling of some Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// The [`Highlight`] to be applied to the background of inline code. + pub inline_code_highlight: Highlight, + /// The [`Padding`] to be applied to the background of inline code. + pub inline_code_padding: Padding, + /// The [`Color`] to be applied to inline code. + pub inline_code_color: Color, + /// The [`Color`] to be applied to links. + pub link_color: Color, +} + +impl Style { + /// Creates a new [`Style`] from the given [`theme::Palette`]. + pub fn from_palette(palette: theme::Palette) -> Self { + Self { + inline_code_padding: padding::left(1).right(1), + inline_code_highlight: Highlight { + background: color!(0x111).into(), + border: border::rounded(2), + }, + inline_code_color: Color::WHITE, + link_color: palette.primary, + } + } +} + /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. pub fn view<'a, Theme, Renderer>( items: impl IntoIterator<Item = &'a Item>, settings: Settings, + style: Style, ) -> Element<'a, Url, Theme, Renderer> where Theme: Catalog + 'a, @@ -372,7 +508,7 @@ where let blocks = items.into_iter().enumerate().map(|(i, item)| match item { Item::Heading(level, heading) => { - container(rich_text(heading).size(match level { + container(rich_text(heading.spans(style)).size(match level { pulldown_cmark::HeadingLevel::H1 => h1_size, pulldown_cmark::HeadingLevel::H2 => h2_size, pulldown_cmark::HeadingLevel::H3 => h3_size, @@ -388,11 +524,11 @@ where .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).size(text_size).into() + rich_text(paragraph.spans(style)).size(text_size).into() } Item::List { start: None, items } => { column(items.iter().map(|items| { - row![text("•").size(text_size), view(items, settings)] + row![text("•").size(text_size), view(items, settings, style)] .spacing(spacing) .into() })) @@ -405,7 +541,7 @@ where } => column(items.iter().enumerate().map(|(i, items)| { row![ text!("{}.", i as u64 + *start).size(text_size), - view(items, settings) + view(items, settings, style) ] .spacing(spacing) .into() @@ -415,7 +551,9 @@ where Item::CodeBlock(code) => container( scrollable( container( - rich_text(code).font(Font::MONOSPACE).size(code_size), + rich_text(code.spans(style)) + .font(Font::MONOSPACE) + .size(code_size), ) .padding(spacing.0 / 2.0), ) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index c6aa1e14..1eb0d296 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -13,8 +13,6 @@ use crate::core::{ Rectangle, Shell, Size, Vector, Widget, }; -use std::borrow::Cow; - /// A bunch of [`Rich`] text. #[allow(missing_debug_implementations)] pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer> @@ -23,7 +21,7 @@ where Theme: Catalog, Renderer: core::text::Renderer, { - spans: Cow<'a, [Span<'a, Link, Renderer::Font>]>, + spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -39,11 +37,12 @@ where Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, + Renderer::Font: 'a, { /// Creates a new empty [`Rich`] text. pub fn new() -> Self { Self { - spans: Cow::default(), + spans: Box::new([]), size: None, line_height: LineHeight::default(), width: Length::Shrink, @@ -57,10 +56,10 @@ where /// Creates a new [`Rich`] text with the given text spans. pub fn with_spans( - spans: impl Into<Cow<'a, [Span<'a, Link, Renderer::Font>]>>, + spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a, ) -> Self { Self { - spans: spans.into(), + spans: Box::new(spans), ..Self::new() } } @@ -154,15 +153,6 @@ where self.class = class.into(); self } - - /// Adds a new text [`Span`] to the [`Rich`] text. - pub fn push( - mut self, - span: impl Into<Span<'a, Link, Renderer::Font>>, - ) -> Self { - self.spans.to_mut().push(span.into()); - self - } } impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer> @@ -170,6 +160,7 @@ where Link: Clone + 'a, Theme: Catalog, Renderer: core::text::Renderer, + Renderer::Font: 'a, { fn default() -> Self { Self::new() @@ -221,7 +212,7 @@ where limits, self.width, self.height, - self.spans.as_ref(), + self.spans.as_ref().as_ref(), self.line_height, self.size, self.font, @@ -250,7 +241,7 @@ where .position_in(layout.bounds()) .and_then(|position| state.paragraph.hit_span(position)); - for (index, span) in self.spans.iter().enumerate() { + for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() { let is_hovered_link = span.link.is_some() && Some(index) == hovered_span; @@ -394,6 +385,8 @@ where Some(span) if span == span_pressed => { if let Some(link) = self .spans + .as_ref() + .as_ref() .get(span) .and_then(|span| span.link.clone()) { @@ -427,7 +420,7 @@ where if let Some(span) = state .paragraph .hit_span(position) - .and_then(|span| self.spans.get(span)) + .and_then(|span| self.spans.as_ref().as_ref().get(span)) { if span.link.is_some() { return mouse::Interaction::Pointer; @@ -509,14 +502,12 @@ where Link: Clone + 'a, Theme: Catalog, Renderer: core::text::Renderer, + Renderer::Font: 'a, { fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>( spans: T, ) -> Self { - Self { - spans: spans.into_iter().collect(), - ..Self::new() - } + Self::with_spans(spans.into_iter().collect::<Vec<_>>()) } } From bb6fa4292433c015cb5b69a2c4f7d7f0b92339c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 22 Aug 2024 02:30:12 +0200 Subject: [PATCH 216/657] Fix ambiguous `rich_text` link in `widget::markdown` --- 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 ef4da0df..fa4ee6bf 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -57,7 +57,7 @@ impl Text { } } - /// Returns the [`rich_text`] spans ready to be used for the given style. + /// Returns the [`rich_text()`] spans ready to be used for the given style. /// /// This method performs caching for you. It will only reallocate if the [`Style`] /// provided changes. From 3a434c9505bcc1a9ce71db1d69f77c3374b076cb Mon Sep 17 00:00:00 2001 From: mtkennerly <mtkennerly@gmail.com> Date: Thu, 22 Aug 2024 09:32:35 -0400 Subject: [PATCH 217/657] Add compact variant for pane grid controls --- examples/pane_grid/src/main.rs | 22 ++- widget/src/pane_grid.rs | 2 + widget/src/pane_grid/controls.rs | 59 ++++++ widget/src/pane_grid/title_bar.rs | 313 +++++++++++++++++++++++------- 4 files changed, 324 insertions(+), 72 deletions(-) create mode 100644 widget/src/pane_grid/controls.rs diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index f18fc5f3..67f4d27f 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -154,11 +154,23 @@ impl Example { .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls( - id, - total_panes, - pane.is_pinned, - is_maximized, + .controls(pane_grid::Controls::dynamic( + view_controls( + id, + total_panes, + pane.is_pinned, + is_maximized, + ), + button(text("X").size(14)) + .style(button::danger) + .padding(3) + .on_press_maybe( + if total_panes > 1 && !pane.is_pinned { + Some(Message::Close(id)) + } else { + None + }, + ), )) .padding(10) .style(if is_focused { diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 0aab1ab5..710a5443 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -10,6 +10,7 @@ mod axis; mod configuration; mod content; +mod controls; mod direction; mod draggable; mod node; @@ -22,6 +23,7 @@ pub mod state; pub use axis::Axis; pub use configuration::Configuration; pub use content::Content; +pub use controls::Controls; pub use direction::Direction; pub use draggable::Draggable; pub use node::Node; diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs new file mode 100644 index 00000000..13b57acb --- /dev/null +++ b/widget/src/pane_grid/controls.rs @@ -0,0 +1,59 @@ +use crate::container; +use crate::core::{self, Element}; + +/// The controls of a [`Pane`]. +/// +/// [`Pane`]: super::Pane +#[allow(missing_debug_implementations)] +pub struct Controls< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + pub(super) full: Element<'a, Message, Theme, Renderer>, + pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>, +} + +impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer> +where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + /// Creates a new [`Controls`] with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + full: content.into(), + compact: None, + } + } + + /// Creates a new [`Controls`] with a full and compact variant. + /// If there is not enough room to show the full variant without overlap, + /// then the compact variant will be shown instead. + pub fn dynamic( + full: impl Into<Element<'a, Message, Theme, Renderer>>, + compact: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + full: full.into(), + compact: Some(compact.into()), + } + } +} + +impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>> + for Controls<'a, Message, Theme, Renderer> +where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + fn from(value: Element<'a, Message, Theme, Renderer>) -> Self { + Self::new(value) + } +} diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 791fab4a..5002b4f7 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -9,6 +9,7 @@ use crate::core::{ self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, Vector, }; +use crate::pane_grid::controls::Controls; /// The title bar of a [`Pane`]. /// @@ -24,7 +25,7 @@ pub struct TitleBar< Renderer: core::Renderer, { content: Element<'a, Message, Theme, Renderer>, - controls: Option<Element<'a, Message, Theme, Renderer>>, + controls: Option<Controls<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, class: Theme::Class<'a>, @@ -51,7 +52,7 @@ where /// Sets the controls of the [`TitleBar`]. pub fn controls( mut self, - controls: impl Into<Element<'a, Message, Theme, Renderer>>, + controls: impl Into<Controls<'a, Message, Theme, Renderer>>, ) -> Self { self.controls = Some(controls.into()); self @@ -104,10 +105,22 @@ where Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { - let children = if let Some(controls) = self.controls.as_ref() { - vec![Tree::new(&self.content), Tree::new(controls)] - } else { - vec![Tree::new(&self.content), Tree::empty()] + let children = match self.controls.as_ref() { + Some(controls) => match controls.compact.as_ref() { + Some(compact) => vec![ + Tree::new(&self.content), + Tree::new(&controls.full), + Tree::new(compact), + ], + None => vec![ + Tree::new(&self.content), + Tree::new(&controls.full), + Tree::empty(), + ], + }, + None => { + vec![Tree::new(&self.content), Tree::empty(), Tree::empty()] + } }; Tree { @@ -117,9 +130,13 @@ where } pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { + if tree.children.len() == 3 { if let Some(controls) = self.controls.as_ref() { - tree.children[1].diff(controls); + if let Some(compact) = controls.compact.as_ref() { + tree.children[2].diff(compact); + } + + tree.children[1].diff(&controls.full); } tree.children[0].diff(&self.content); @@ -164,18 +181,42 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); - controls.as_widget().draw( - &tree.children[1], - renderer, - theme, - &inherited_style, - controls_layout, - cursor, - viewport, - ); + compact.as_widget().draw( + &tree.children[2], + renderer, + theme, + &inherited_style, + compact_layout, + cursor, + viewport, + ); + } else { + show_title = false; + + controls.full.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); + } + } else { + controls.full.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); + } } } @@ -207,13 +248,20 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - if self.controls.is_some() { + if let Some(controls) = self.controls.as_ref() { let controls_layout = children.next().unwrap(); if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - !controls_layout.bounds().contains(cursor_position) + if controls.compact.is_some() { + let compact_layout = children.next().unwrap(); + + !compact_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } else { + !controls_layout.bounds().contains(cursor_position) + } } else { !controls_layout.bounds().contains(cursor_position) && !title_layout.bounds().contains(cursor_position) @@ -244,25 +292,73 @@ where let title_size = title_layout.size(); let node = if let Some(controls) = &self.controls { - let controls_layout = controls.as_widget().layout( + let controls_layout = controls.full.as_widget().layout( &mut tree.children[1], renderer, &layout::Limits::new(Size::ZERO, max_size), ); - let controls_size = controls_layout.size(); - let space_before_controls = max_size.width - controls_size.width; + if title_layout.bounds().width + controls_layout.bounds().width + > max_size.width + { + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = compact.as_widget().layout( + &mut tree.children[2], + renderer, + &layout::Limits::new(Size::ZERO, max_size), + ); - let height = title_size.height.max(controls_size.height); + let compact_size = compact_layout.size(); + let space_before_controls = + max_size.width - compact_size.width; - layout::Node::with_children( - Size::new(max_size.width, height), - vec![ - title_layout, - controls_layout - .move_to(Point::new(space_before_controls, 0.0)), - ], - ) + let height = title_size.height.max(compact_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout, + compact_layout.move_to(Point::new( + space_before_controls, + 0.0, + )), + ], + ) + } else { + let controls_size = controls_layout.size(); + let space_before_controls = + max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout.move_to(Point::new( + space_before_controls, + 0.0, + )), + ], + ) + } + } else { + let controls_size = controls_layout.size(); + let space_before_controls = + max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout + .move_to(Point::new(space_before_controls, 0.0)), + ], + ) + } } else { layout::Node::with_children( Size::new(max_size.width, title_size.height), @@ -293,15 +389,33 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); - controls.as_widget().operate( - &mut tree.children[1], - controls_layout, - renderer, - operation, - ); + compact.as_widget().operate( + &mut tree.children[2], + compact_layout, + renderer, + operation, + ); + } else { + show_title = false; + + controls.full.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ); + } + } else { + controls.full.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ); + } }; if show_title { @@ -337,19 +451,45 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_mut() { + let compact_layout = children.next().unwrap(); - controls.as_widget_mut().on_event( - &mut tree.children[1], - event.clone(), - controls_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) + compact.as_widget_mut().on_event( + &mut tree.children[2], + event.clone(), + compact_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } else { + show_title = false; + + controls.full.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + } else { + controls.full.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } } else { event::Status::Ignored }; @@ -396,18 +536,33 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let controls_interaction = controls.as_widget().mouse_interaction( - &tree.children[1], - controls_layout, - cursor, - viewport, - renderer, - ); + let controls_interaction = + controls.full.as_widget().mouse_interaction( + &tree.children[1], + controls_layout, + cursor, + viewport, + renderer, + ); if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - controls_interaction + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); + let compact_interaction = + compact.as_widget().mouse_interaction( + &tree.children[2], + compact_layout, + cursor, + viewport, + renderer, + ); + + compact_interaction.max(title_interaction) + } else { + controls_interaction + } } else { controls_interaction.max(title_interaction) } @@ -444,12 +599,36 @@ where controls.as_mut().and_then(|controls| { let controls_layout = children.next()?; - controls.as_widget_mut().overlay( - controls_state, - controls_layout, - renderer, - translation, - ) + if title_layout.bounds().width + + controls_layout.bounds().width + > padded.bounds().width + { + if let Some(compact) = controls.compact.as_mut() { + let compact_state = states.next().unwrap(); + let compact_layout = children.next()?; + + compact.as_widget_mut().overlay( + compact_state, + compact_layout, + renderer, + translation, + ) + } else { + controls.full.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + translation, + ) + } + } else { + controls.full.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + translation, + ) + } }) }) } From 6c741923c64c54841bc4f3a8a1b694975ee2eed7 Mon Sep 17 00:00:00 2001 From: Shan <shankern@protonmail.com> Date: Tue, 30 Jul 2024 14:09:12 -0700 Subject: [PATCH 218/657] Implement `align_x` for `TextInput` Co-authored-by: Shan <shankern@protonmail.com> --- examples/todos/src/main.rs | 3 +- widget/src/text_input.rs | 79 +++++++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 86845f87..a5f7b36a 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -202,7 +202,8 @@ impl Todos { .on_input(Message::InputChanged) .on_submit(Message::CreateTask) .padding(15) - .size(30); + .size(30) + .align_x(Center); let controls = view_controls(tasks, *filter); let filtered_tasks = diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 173de136..2ac6f4ba 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -19,7 +19,7 @@ use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::paragraph; +use crate::core::text::paragraph::{self, Paragraph as _}; use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; @@ -74,6 +74,7 @@ pub struct TextInput< padding: Padding, size: Option<Pixels>, line_height: text::LineHeight, + alignment: alignment::Horizontal, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, @@ -103,6 +104,7 @@ where padding: DEFAULT_PADDING, size: None, line_height: text::LineHeight::default(), + alignment: alignment::Horizontal::Left, on_input: None, on_paste: None, on_submit: None, @@ -193,6 +195,15 @@ where self } + /// Sets the horizontal alignment of the [`TextInput`]. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.alignment = alignment.into(); + self + } + /// Sets the style of the [`TextInput`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -457,9 +468,21 @@ where }; let draw = |renderer: &mut Renderer, viewport| { + let paragraph = if text.is_empty() { + state.placeholder.raw() + } else { + state.value.raw() + }; + + let alignment_offset = alignment_offset( + text_bounds.width, + paragraph.min_width(), + self.alignment, + ); + if let Some((cursor, color)) = cursor { renderer.with_translation( - Vector::new(-offset, 0.0), + Vector::new(alignment_offset - offset, 0.0), |renderer| { renderer.fill_quad(cursor, color); }, @@ -469,13 +492,9 @@ where } renderer.fill_paragraph( - if text.is_empty() { - state.placeholder.raw() - } else { - state.value.raw() - }, + paragraph, Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), + + Vector::new(alignment_offset - offset, 0.0), if text.is_empty() { style.placeholder } else { @@ -600,7 +619,18 @@ where if let Some(cursor_position) = click_position { let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; + + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + self.alignment, + ); + + cursor_position.x - text_bounds.x - alignment_offset + }; let click = mouse::Click::new(cursor_position, state.last_click); @@ -677,7 +707,18 @@ where if state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; + + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + self.alignment, + ); + + position.x - text_bounds.x - alignment_offset + }; let value = if self.is_secure { self.value.secure() @@ -1486,3 +1527,21 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, } } + +fn alignment_offset( + text_bounds_width: f32, + text_min_width: f32, + alignment: alignment::Horizontal, +) -> f32 { + if text_min_width > text_bounds_width { + 0.0 + } else { + match alignment { + alignment::Horizontal::Left => 0.0, + alignment::Horizontal::Center => { + (text_bounds_width - text_min_width) / 2.0 + } + alignment::Horizontal::Right => text_bounds_width - text_min_width, + } + } +} From 0dcec519be23da6d3bc409dbf7ac65407d59dc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector0193@gmail.com> Date: Fri, 30 Aug 2024 13:02:49 +0200 Subject: [PATCH 219/657] Add `get_scale_factor` task to `window` module --- runtime/src/window.rs | 10 ++++++++++ winit/src/program.rs | 9 ++++++++- winit/src/program/window_manager.rs | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index cd27cdfe..ce6fd1b6 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -63,6 +63,9 @@ pub enum Action { /// Get the current logical coordinates of the window. GetPosition(Id, oneshot::Sender<Option<Point>>), + /// Get the current scale factor (DPI) of the window. + GetScaleFactor(Id, oneshot::Sender<f32>), + /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. @@ -292,6 +295,13 @@ pub fn get_position(id: Id) -> Task<Option<Point>> { }) } +/// Gets the scale factor of the window with the given [`Id`]. +pub fn get_scale_factor(id: Id) -> Task<f32> { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetScaleFactor(id, channel)) + }) +} + /// Moves the window to the given logical coordinates. pub fn move_to<T>(id: Id, position: Point) -> Task<T> { task::effect(crate::Action::Window(Action::Move(id, position))) diff --git a/winit/src/program.rs b/winit/src/program.rs index c5c3133d..54221c68 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1291,7 +1291,7 @@ fn run_action<P, C>( } } window::Action::GetPosition(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { + if let Some(window) = window_manager.get(id) { let position = window .raw .inner_position() @@ -1306,6 +1306,13 @@ fn run_action<P, C>( let _ = channel.send(position); } } + window::Action::GetScaleFactor(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let scale_factor = window.raw.scale_factor(); + + let _ = channel.send(scale_factor as f32); + } + } window::Action::Move(id, position) => { if let Some(window) = window_manager.get_mut(id) { window.raw.set_outer_position( diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index fcbf79f6..8cd9fab7 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -80,6 +80,10 @@ where self.entries.iter_mut().map(|(k, v)| (*k, v)) } + pub fn get(&self, id: Id) -> Option<&Window<P, C>> { + self.entries.get(&id) + } + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> { self.entries.get_mut(&id) } From fa66610f246f681f2d9f3ac968ee8a8e1b507dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector0193@gmail.com> Date: Mon, 2 Sep 2024 11:47:55 +0200 Subject: [PATCH 220/657] Introduce `image-without-codecs` feature flag Co-authored-by: dtzxporter <dtzxporter@users.noreply.github.com> --- Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65c5007e..52464e38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,9 @@ wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enable the `tiny-skia` software renderer backend tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `Image` widget -image = ["iced_widget/image", "dep:image"] +image = ["image-without-codecs", "image/default"] +# Enables the `Image` widget, without any built-in codecs of the `image` crate +image-without-codecs = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget svg = ["iced_widget/svg"] # Enables the `Canvas` widget @@ -147,7 +149,7 @@ glam = "0.25" glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" } guillotiere = "0.6" half = "2.2" -image = "0.24" +image = { version = "0.24", default-features = false } kamadak-exif = "0.5" kurbo = "0.10" log = "0.4" From feff4d1cba1766a0e4901865f268cc90e7618c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector0193@gmail.com> Date: Mon, 2 Sep 2024 11:59:35 +0200 Subject: [PATCH 221/657] Introduce `container::background` style helper --- widget/src/container.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/widget/src/container.rs b/widget/src/container.rs index ba315741..7eb50120 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -629,6 +629,11 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style { Style::default() } +/// A [`Container`] with the given [`Background`]. +pub fn background(background: impl Into<Background>) -> Style { + Style::default().background(background) +} + /// A rounded [`Container`] with a background. pub fn rounded_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); From 9d7aa116238465cdc23ff7de868e7ff3b77db6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector0193@gmail.com> Date: Mon, 2 Sep 2024 11:59:45 +0200 Subject: [PATCH 222/657] Implement `From<Style>` for `container::StyleFn` Co-authored-by: wiiznokes <78230769+wiiznokes@users.noreply.github.com> --- widget/src/container.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/widget/src/container.rs b/widget/src/container.rs index 7eb50120..c3a66360 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -612,6 +612,12 @@ pub trait Catalog { /// A styling function for a [`Container`]. pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; +impl<'a, Theme> From<Style> for StyleFn<'a, Theme> { + fn from(style: Style) -> Self { + Box::new(move |_theme| style) + } +} + impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; From 0d298b70d23663b6e230b41818442b4f1a91f14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ki=C3=ABd=20Llaentenn?= <kiedtl@tilde.team> Date: Mon, 2 Sep 2024 09:03:48 -0400 Subject: [PATCH 223/657] slider: handle mouse wheel events --- widget/src/slider.rs | 16 ++++++++++++++++ widget/src/vertical_slider.rs | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index e586684a..130c9bf3 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -288,6 +288,22 @@ where }; match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(_) = cursor.position_over(layout.bounds()) { + let delta = match delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; + + if delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { + let _ = increment(current_value).map(change); + } + + return event::Status::Captured; + } + } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index f21b996c..5a3519f4 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -291,6 +291,22 @@ where }; match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(_) = cursor.position_over(layout.bounds()) { + let delta = match delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; + + if delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { + let _ = increment(current_value).map(change); + } + + return event::Status::Captured; + } + } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = From 9628dc20d5dab128b9fff2c4b73cc66b0071e149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 3 Sep 2024 11:23:54 +0200 Subject: [PATCH 224/657] Reconnect `Clipboard` on window close Fixes #2564 --- winit/src/clipboard.rs | 10 +++++- winit/src/program.rs | 54 ++++++++++++++++++----------- winit/src/program/window_manager.rs | 4 +++ 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 7ae646fc..d54a1fe0 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -2,7 +2,7 @@ use crate::core::clipboard::Kind; use std::sync::Arc; -use winit::window::Window; +use winit::window::{Window, WindowId}; /// A buffer for short-term storage and transfer within and between /// applications. @@ -83,6 +83,14 @@ impl Clipboard { State::Unavailable => {} } } + + /// Returns the identifier of the window used to create the [`Clipboard`], if any. + pub fn window_id(&self) -> Option<WindowId> { + match &self.state { + State::Connected { window, .. } => Some(window.id()), + State::Unavailable => None, + } + } } impl crate::core::Clipboard for Clipboard { diff --git a/winit/src/program.rs b/winit/src/program.rs index 54221c68..89ec5ef9 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -307,8 +307,6 @@ where } }; - let clipboard = Clipboard::connect(window.clone()); - let finish_boot = async move { let mut compositor = C::new(graphics_settings, window.clone()).await?; @@ -318,10 +316,7 @@ where } sender - .send(Boot { - compositor, - clipboard, - }) + .send(Boot { compositor }) .ok() .expect("Send boot event"); @@ -617,7 +612,6 @@ where struct Boot<C> { compositor: C, - clipboard: Clipboard, } #[derive(Debug)] @@ -662,10 +656,7 @@ async fn run_instance<P, C>( use winit::event; use winit::event_loop::ControlFlow; - let Boot { - mut compositor, - mut clipboard, - } = boot.await.expect("Receive boot"); + let Boot { mut compositor } = boot.await.expect("Receive boot"); let mut window_manager = WindowManager::new(); let mut is_window_opening = !is_daemon; @@ -676,6 +667,7 @@ async fn run_instance<P, C>( let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); + let mut clipboard = Clipboard::unconnected(); debug.startup_finished(); @@ -734,6 +726,10 @@ async fn run_instance<P, C>( }), )); + if clipboard.window_id().is_none() { + clipboard = Clipboard::connect(window.raw.clone()); + } + let _ = on_open.send(id); is_window_opening = false; } @@ -979,14 +975,22 @@ async fn run_instance<P, C>( winit::event::WindowEvent::CloseRequested ) && window.exit_on_close_request { - let _ = window_manager.remove(id); - let _ = user_interfaces.remove(&id); - let _ = ui_caches.remove(&id); - - events.push(( - id, - core::Event::Window(window::Event::Closed), - )); + run_action( + Action::Window(runtime::window::Action::Close( + id, + )), + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); } else { window.state.update( &window.raw, @@ -1223,10 +1227,18 @@ fn run_action<P, C>( *is_window_opening = true; } window::Action::Close(id) => { - let window = window_manager.remove(id); let _ = ui_caches.remove(&id); + let _ = interfaces.remove(&id); + + if let Some(window) = window_manager.remove(id) { + if clipboard.window_id() == Some(window.raw.id()) { + *clipboard = window_manager + .first() + .map(|window| window.raw.clone()) + .map(Clipboard::connect) + .unwrap_or_else(Clipboard::unconnected); + } - if window.is_some() { events.push(( id, core::Event::Window(core::window::Event::Closed), diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 8cd9fab7..3d22e155 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -74,6 +74,10 @@ where self.entries.is_empty() } + pub fn first(&self) -> Option<&Window<P, C>> { + self.entries.first_key_value().map(|(_id, window)| window) + } + pub fn iter_mut( &mut self, ) -> impl Iterator<Item = (Id, &mut Window<P, C>)> { From 9572bd1e9090fddb69e38b178ec7545630476c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 4 Sep 2024 20:27:28 +0200 Subject: [PATCH 225/657] Allow interactions on disabled `text_input` Co-authored-by: Daniel Yoon <101683475+Koranir@users.noreply.github.com> --- widget/src/text_input.rs | 47 ++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 2ac6f4ba..52e37388 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -395,11 +395,11 @@ where position, ); - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; + let is_cursor_visible = !is_disabled + && ((focus.now - focus.updated_at).as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; let cursor = if is_cursor_visible { Some(( @@ -531,12 +531,9 @@ where fn diff(&self, tree: &mut Tree) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - // Unfocus text input if it becomes disabled + // Stop pasting if input becomes disabled if self.on_input.is_none() { - state.last_click = None; - state.is_focused = None; state.is_pasting = None; - state.is_dragging = false; } } @@ -597,11 +594,7 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state::<Renderer>(tree); - let click_position = if self.on_input.is_some() { - cursor.position_over(layout.bounds()) - } else { - None - }; + let click_position = cursor.position_over(layout.bounds()); state.is_focused = if click_position.is_some() { state.is_focused.or_else(|| { @@ -747,10 +740,6 @@ where let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { - let Some(on_input) = &self.on_input else { - return event::Status::Ignored; - }; - let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -774,6 +763,10 @@ where if state.keyboard_modifiers.command() && !self.is_secure => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + if let Some((start, end)) = state.cursor.selection(&self.value) { @@ -798,6 +791,10 @@ where if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + let content = match state.is_pasting.take() { Some(content) => content, None => { @@ -841,6 +838,10 @@ where } if let Some(text) = text { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + state.is_pasting = None; if let Some(c) = @@ -869,6 +870,10 @@ where } } keyboard::Key::Named(key::Named::Backspace) => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { @@ -893,6 +898,10 @@ where update_cache(state, &self.value); } keyboard::Key::Named(key::Named::Delete) => { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { @@ -1111,7 +1120,7 @@ where ) -> mouse::Interaction { if cursor.is_over(layout.bounds()) { if self.on_input.is_none() { - mouse::Interaction::NotAllowed + mouse::Interaction::Idle } else { mouse::Interaction::Text } From f98328f4f1ee58b6288e4f19d7475e7eeb9a7ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 4 Sep 2024 21:25:59 +0200 Subject: [PATCH 226/657] Add `text::Wrapping` support Co-authored-by: Neeraj Jaiswal <neerajj85@gmail.com> --- core/src/renderer/null.rs | 1 + core/src/text.rs | 19 +++++++++++++++++++ core/src/text/editor.rs | 3 ++- core/src/text/paragraph.rs | 1 + core/src/widget/text.rs | 15 +++++++++++++-- examples/editor/src/main.rs | 20 +++++++++++++++++++- examples/styling/src/main.rs | 2 +- examples/tour/src/main.rs | 2 +- graphics/src/text.rs | 12 +++++++++++- graphics/src/text/editor.rs | 27 ++++++++++++++++----------- graphics/src/text/paragraph.rs | 7 ++++++- wgpu/src/lib.rs | 1 + widget/src/checkbox.rs | 12 +++++++++++- widget/src/helpers.rs | 2 +- widget/src/overlay/menu.rs | 1 + widget/src/pick_list.rs | 5 ++++- widget/src/radio.rs | 13 +++++++++++-- widget/src/text/rich.rs | 14 +++++++++++++- widget/src/text_editor.rs | 13 ++++++++++++- widget/src/text_input.rs | 3 +++ widget/src/toggler.rs | 17 +++++++++++++---- 21 files changed, 160 insertions(+), 30 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e3a07280..bbcdd8ff 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -161,6 +161,7 @@ impl text::Editor for () { _new_font: Self::Font, _new_size: Pixels, _new_line_height: text::LineHeight, + _new_wrapping: text::Wrapping, _new_highlighter: &mut impl text::Highlighter, ) { } diff --git a/core/src/text.rs b/core/src/text.rs index dc8f5785..d7b7fee4 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -41,6 +41,9 @@ pub struct Text<Content = String, Font = crate::Font> { /// The [`Shaping`] strategy of the [`Text`]. pub shaping: Shaping, + + /// The [`Wrapping`] strategy of the [`Text`]. + pub wrapping: Wrapping, } /// The shaping strategy of some text. @@ -67,6 +70,22 @@ pub enum Shaping { Advanced, } +/// The wrapping strategy of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum Wrapping { + /// No wrapping. + None, + /// Wraps at the word level. + /// + /// This is the default. + #[default] + Word, + /// Wraps at the glyph level. + Glyph, + /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself. + WordOrGlyph, +} + /// The height of a line of text in a paragraph. #[derive(Debug, Clone, Copy, PartialEq)] pub enum LineHeight { diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 135707d1..cd30db3a 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,6 +1,6 @@ //! Edit text. use crate::text::highlighter::{self, Highlighter}; -use crate::text::LineHeight; +use crate::text::{LineHeight, Wrapping}; use crate::{Pixels, Point, Rectangle, Size}; use std::sync::Arc; @@ -50,6 +50,7 @@ pub trait Editor: Sized + Default { new_font: Self::Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ); diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 04a97f35..924276c3 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -95,6 +95,7 @@ impl<P: Paragraph> Plain<P> { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, }) { Difference::None => {} Difference::Bounds => { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 5c5b78dd..d8d6e4c6 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -11,7 +11,7 @@ use crate::{ Widget, }; -pub use text::{LineHeight, Shaping}; +pub use text::{LineHeight, Shaping, Wrapping}; /// A paragraph of text. #[allow(missing_debug_implementations)] @@ -29,6 +29,7 @@ where vertical_alignment: alignment::Vertical, font: Option<Renderer::Font>, shaping: Shaping, + wrapping: Wrapping, class: Theme::Class<'a>, } @@ -48,7 +49,8 @@ where height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::default(), + wrapping: Wrapping::default(), class: Theme::default(), } } @@ -115,6 +117,12 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`Text`]. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + /// Sets the style of the [`Text`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self @@ -198,6 +206,7 @@ where self.horizontal_alignment, self.vertical_alignment, self.shaping, + self.wrapping, ) } @@ -232,6 +241,7 @@ pub fn layout<Renderer>( horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, + wrapping: Wrapping, ) -> layout::Node where Renderer: text::Renderer, @@ -253,6 +263,7 @@ where horizontal_alignment, vertical_alignment, shaping, + wrapping, }); paragraph.min_bounds() diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index aa07b328..5f12aec5 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -2,7 +2,7 @@ use iced::highlighter; use iced::keyboard; use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, - text_editor, tooltip, + text_editor, toggler, tooltip, }; use iced::{Center, Element, Fill, Font, Subscription, Task, Theme}; @@ -24,6 +24,7 @@ struct Editor { file: Option<PathBuf>, content: text_editor::Content, theme: highlighter::Theme, + word_wrap: bool, is_loading: bool, is_dirty: bool, } @@ -32,6 +33,7 @@ struct Editor { enum Message { ActionPerformed(text_editor::Action), ThemeSelected(highlighter::Theme), + WordWrapToggled(bool), NewFile, OpenFile, FileOpened(Result<(PathBuf, Arc<String>), Error>), @@ -46,6 +48,7 @@ impl Editor { file: None, content: text_editor::Content::new(), theme: highlighter::Theme::SolarizedDark, + word_wrap: true, is_loading: true, is_dirty: false, }, @@ -76,6 +79,11 @@ impl Editor { Task::none() } + Message::WordWrapToggled(word_wrap) => { + self.word_wrap = word_wrap; + + Task::none() + } Message::NewFile => { if !self.is_loading { self.file = None; @@ -152,6 +160,11 @@ impl Editor { self.is_dirty.then_some(Message::SaveFile) ), horizontal_space(), + toggler( + Some("Word Wrap"), + self.word_wrap, + Message::WordWrapToggled + ), pick_list( highlighter::Theme::ALL, Some(self.theme), @@ -189,6 +202,11 @@ impl Editor { text_editor(&self.content) .height(Fill) .on_action(Message::ActionPerformed) + .wrapping(if self.word_wrap { + text::Wrapping::Word + } else { + text::Wrapping::None + }) .highlight( self.file .as_deref() diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 527aaa29..e19d5cf7 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -78,7 +78,7 @@ impl Styling { .on_toggle(Message::CheckboxToggled); let toggler = toggler( - String::from("Toggle me!"), + Some("Toggle me!"), self.toggler_value, Message::TogglerToggled, ) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index ee4754e6..0dd588fe 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -358,7 +358,7 @@ impl Tour { .push("A toggler is mostly used to enable or disable something.") .push( Container::new(toggler( - "Toggle me to continue...".to_owned(), + Some("Toggle me to continue..."), self.toggler, Message::TogglerChanged, )) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 23ec14d4..feb9932a 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -11,7 +11,7 @@ pub use cosmic_text; use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrapping}; use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; @@ -306,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap { + match wrapping { + Wrapping::None => cosmic_text::Wrap::None, + Wrapping::Word => cosmic_text::Wrap::Word, + Wrapping::Glyph => cosmic_text::Wrap::Glyph, + Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 80733bbb..fe1442d1 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -3,7 +3,7 @@ use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::LineHeight; +use crate::core::text::{LineHeight, Wrapping}; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -437,6 +437,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ) { let editor = @@ -448,13 +449,12 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); + let buffer = buffer_mut_from_editor(&mut internal.editor); + if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in buffer_mut_from_editor(&mut internal.editor) - .lines - .iter_mut() - { + for line in buffer.lines.iter_mut() { line.reset(); } @@ -465,10 +465,7 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in buffer_mut_from_editor(&mut internal.editor) - .lines - .iter_mut() - { + for line in buffer.lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); @@ -478,7 +475,7 @@ impl editor::Editor for Editor { internal.topmost_line_changed = Some(0); } - let metrics = buffer_from_editor(&internal.editor).metrics(); + let metrics = buffer.metrics(); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -486,12 +483,20 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - buffer_mut_from_editor(&mut internal.editor).set_metrics( + buffer.set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); } + let new_wrap = text::to_wrap(new_wrapping); + + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); + + buffer.set_wrap(font_system.raw(), new_wrap); + } + if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index b9f9c833..07ddbb82 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,7 +1,7 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, Shaping, Span, Text}; +use crate::core::text::{Hit, Shaping, Span, Text, Wrapping}; use crate::core::{Font, Point, Rectangle, Size}; use crate::text; @@ -17,6 +17,7 @@ struct Internal { buffer: cosmic_text::Buffer, font: Font, shaping: Shaping, + wrapping: Wrapping, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, bounds: Size, @@ -94,6 +95,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, bounds: text.bounds, min_bounds, version: font_system.version(), @@ -160,6 +162,7 @@ impl core::text::Paragraph for Paragraph { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, bounds: text.bounds, min_bounds, version: font_system.version(), @@ -192,6 +195,7 @@ impl core::text::Paragraph for Paragraph { || metrics.line_height != text.line_height.to_absolute(text.size).0 || paragraph.font != text.font || paragraph.shaping != text.shaping + || paragraph.wrapping != text.wrapping || paragraph.horizontal_alignment != text.horizontal_alignment || paragraph.vertical_alignment != text.vertical_alignment { @@ -387,6 +391,7 @@ impl Default for Internal { }), font: Font::default(), shaping: Shaping::default(), + wrapping: Wrapping::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, bounds: Size::ZERO, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 39167514..d79f0dc8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -408,6 +408,7 @@ impl Renderer { horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: core::text::Shaping::Basic, + wrapping: core::text::Wrapping::Word, }; renderer.fill_text( diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index e5abfbb4..32db5090 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -50,6 +50,7 @@ pub struct Checkbox< text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, class: Theme::Class<'a>, @@ -81,7 +82,8 @@ where spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), font: None, icon: Icon { font: Renderer::ICON_FONT, @@ -158,6 +160,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`]. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`]. /// /// [`Renderer::Font`]: crate::core::text::Renderer @@ -240,6 +248,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) @@ -348,6 +357,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: *shaping, + wrapping: text::Wrapping::default(), }, bounds.center(), style.icon_color, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1cb02830..349f02a6 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -767,7 +767,7 @@ where /// /// [`Toggler`]: crate::Toggler pub fn toggler<'a, Message, Theme, Renderer>( - label: impl Into<Option<String>>, + label: Option<impl text::IntoFragment<'a>>, is_checked: bool, f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 73d1cc8c..f05ae40a 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -532,6 +532,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index f7f7b65b..1fc9951e 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -81,7 +81,7 @@ where padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), font: None, handle: Handle::default(), class: <Theme as Catalog>::default(), @@ -250,6 +250,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) @@ -515,6 +516,7 @@ where horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, + wrapping: text::Wrapping::default(), }, Point::new( bounds.x + bounds.width - self.padding.right, @@ -544,6 +546,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 1b02f8ca..cfa961f3 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -82,6 +82,7 @@ where text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, font: Option<Renderer::Font>, class: Theme::Class<'a>, } @@ -122,10 +123,11 @@ where label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, //15 + spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), font: None, class: Theme::default(), } @@ -170,6 +172,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the text font of the [`Radio`] button. pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = Some(font.into()); @@ -245,6 +253,7 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 1eb0d296..921c55a5 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -5,7 +5,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text::{Paragraph, Span}; use crate::core::widget::text::{ - self, Catalog, LineHeight, Shaping, Style, StyleFn, + self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping, }; use crate::core::widget::tree::{self, Tree}; use crate::core::{ @@ -29,6 +29,7 @@ where font: Option<Renderer::Font>, align_x: alignment::Horizontal, align_y: alignment::Vertical, + wrapping: Wrapping, class: Theme::Class<'a>, } @@ -50,6 +51,7 @@ where font: None, align_x: alignment::Horizontal::Left, align_y: alignment::Vertical::Top, + wrapping: Wrapping::default(), class: Theme::default(), } } @@ -118,6 +120,12 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`Rich`] text. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + /// Sets the default style of the [`Rich`] text. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self @@ -218,6 +226,7 @@ where self.font, self.align_x, self.align_y, + self.wrapping, ) } @@ -444,6 +453,7 @@ fn layout<Link, Renderer>( font: Option<Renderer::Font>, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, + wrapping: Wrapping, ) -> layout::Node where Link: Clone, @@ -464,6 +474,7 @@ where horizontal_alignment, vertical_alignment, shaping: Shaping::Advanced, + wrapping, }; if state.spans != spans { @@ -480,6 +491,7 @@ where horizontal_alignment, vertical_alignment, shaping: Shaping::Advanced, + wrapping, }) { core::text::Difference::None => {} core::text::Difference::Bounds => { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 745e3ae8..d1aa4640 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -9,7 +9,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::{self, LineHeight, Text}; +use crate::core::text::{self, LineHeight, Text, Wrapping}; use crate::core::time::{Duration, Instant}; use crate::core::widget::operation; use crate::core::widget::{self, Widget}; @@ -47,6 +47,7 @@ pub struct TextEditor< width: Length, height: Length, padding: Padding, + wrapping: Wrapping, class: Theme::Class<'a>, key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, @@ -74,6 +75,7 @@ where width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), + wrapping: Wrapping::default(), class: Theme::default(), key_binding: None, on_edit: None, @@ -148,6 +150,12 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`TextEditor`]. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + /// Highlights the [`TextEditor`] using the given syntax and theme. #[cfg(feature = "highlighter")] pub fn highlight( @@ -186,6 +194,7 @@ where width: self.width, height: self.height, padding: self.padding, + wrapping: self.wrapping, class: self.class, key_binding: self.key_binding, on_edit: self.on_edit, @@ -496,6 +505,7 @@ where self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, + self.wrapping, state.highlighter.borrow_mut().deref_mut(), ); @@ -784,6 +794,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrapping: self.wrapping, }, text_bounds.position(), style.placeholder, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 52e37388..92047381 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -251,6 +251,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }; state.placeholder.update(placeholder_text); @@ -275,6 +276,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }; state.icon.update(icon_text); @@ -1432,6 +1434,7 @@ fn replace_paragraph<Renderer>( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 821e2526..f7b3078c 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -40,13 +40,14 @@ pub struct Toggler< { is_toggled: bool, on_toggle: Box<dyn Fn(bool) -> Message + 'a>, - label: Option<String>, + label: Option<text::Fragment<'a>>, width: Length, size: f32, text_size: Option<Pixels>, text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, spacing: f32, font: Option<Renderer::Font>, class: Theme::Class<'a>, @@ -69,7 +70,7 @@ where /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. pub fn new<F>( - label: impl Into<Option<String>>, + label: Option<impl text::IntoFragment<'a>>, is_toggled: bool, f: F, ) -> Self @@ -79,13 +80,14 @@ where Toggler { is_toggled, on_toggle: Box::new(f), - label: label.into(), + label: label.map(text::IntoFragment::into_fragment), width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), spacing: Self::DEFAULT_SIZE / 2.0, font: None, class: Theme::default(), @@ -131,6 +133,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Toggler`]. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the spacing between the [`Toggler`] and the text. pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { self.spacing = spacing.into().0; @@ -216,6 +224,7 @@ where self.text_alignment, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) } else { layout::Node::new(Size::ZERO) From 529c459c56a3bfed7a27c1aa798a408680936806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 4 Sep 2024 21:28:44 +0200 Subject: [PATCH 227/657] Remove unnecessary `buffer_mut_from_editor` call --- graphics/src/text/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index fe1442d1..1f1d0050 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -500,7 +500,7 @@ impl editor::Editor for Editor { if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - buffer_mut_from_editor(&mut internal.editor).set_size( + buffer.set_size( font_system.raw(), Some(new_bounds.width), Some(new_bounds.height), From 3a70462a7232cc2b3a7cc3fe8d07f0c29cc578cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Wed, 4 Sep 2024 21:33:07 +0200 Subject: [PATCH 228/657] Fix `toggler` example --- widget/src/toggler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index f7b3078c..57e142e8 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -26,7 +26,7 @@ use crate::core::{ /// /// let is_toggled = true; /// -/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); +/// Toggler::new(Some("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); /// ``` #[allow(missing_debug_implementations)] pub struct Toggler< From 64ec099a9b4490a424d35a4be37c564177e71edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 5 Sep 2024 11:13:37 +0200 Subject: [PATCH 229/657] Add mouse passthrough tasks to `window` module Co-authored-by: Jose Quesada <jquesada2016@fau.edu> --- runtime/src/window.rs | 28 ++++++++++++++++++++++++++++ winit/src/program.rs | 10 ++++++++++ 2 files changed, 38 insertions(+) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index ce6fd1b6..cdf3d80a 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -147,6 +147,18 @@ pub enum Action { /// Screenshot the viewport of the window. Screenshot(Id, oneshot::Sender<Screenshot>), + + /// Enables mouse passthrough for the given window. + /// + /// This disables mouse events for the window and passes mouse events + /// through to whatever window is underneath. + EnableMousePassthrough(Id), + + /// Disable mouse passthrough for the given window. + /// + /// This enables mouse events for the window and stops mouse events + /// from being passed to whatever is underneath. + DisableMousePassthrough(Id), } /// Subscribes to the frames of the window of the running application. @@ -406,3 +418,19 @@ pub fn screenshot(id: Id) -> Task<Screenshot> { crate::Action::Window(Action::Screenshot(id, channel)) }) } + +/// Enables mouse passthrough for the given window. +/// +/// This disables mouse events for the window and passes mouse events +/// through to whatever window is underneath. +pub fn enable_mouse_passthrough<Message>(id: Id) -> Task<Message> { + task::effect(crate::Action::Window(Action::EnableMousePassthrough(id))) +} + +/// Disable mouse passthrough for the given window. +/// +/// This enables mouse events for the window and stops mouse events +/// from being passed to whatever is underneath. +pub fn disable_mouse_passthrough<Message>(id: Id) -> Task<Message> { + task::effect(crate::Action::Window(Action::DisableMousePassthrough(id))) +} diff --git a/winit/src/program.rs b/winit/src/program.rs index 89ec5ef9..52d8eb5f 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1435,6 +1435,16 @@ fn run_action<P, C>( )); } } + window::Action::EnableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(false); + } + } + window::Action::DisableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(true); + } + } }, Action::System(action) => match action { system::Action::QueryInformation(_channel) => { From d1ceada11996a0137e8fb4377f1011af3f08d24f Mon Sep 17 00:00:00 2001 From: Night_Hunter <samuelhuntnz@gmail.com> Date: Thu, 5 Sep 2024 21:17:44 +1200 Subject: [PATCH 230/657] add option for undecorated_shadow on windows (#2285) * add option for undecorated_shadow on windows * formated --- core/src/window/settings/windows.rs | 7 +++++++ winit/src/conversion.rs | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs index 88fe2fbd..a47582a6 100644 --- a/core/src/window/settings/windows.rs +++ b/core/src/window/settings/windows.rs @@ -8,6 +8,12 @@ pub struct PlatformSpecific { /// Whether show or hide the window icon in the taskbar. pub skip_taskbar: bool, + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + pub undecorated_shadow: bool, } impl Default for PlatformSpecific { @@ -15,6 +21,7 @@ impl Default for PlatformSpecific { Self { drag_and_drop: true, skip_taskbar: false, + undecorated_shadow: false, } } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index e88ff84d..cc1959eb 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -79,6 +79,10 @@ pub fn window_attributes( attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); + + window_builder = window_builder.with_undecorated_shadow( + settings.platform_specific.undecorated_shadow, + ); } #[cfg(target_os = "macos")] From 14e686cd37c29e404c33ac8e94a64fe411d9075a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 5 Sep 2024 11:30:25 +0200 Subject: [PATCH 231/657] Fix `winit::conversion` on Windows --- winit/src/conversion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index cc1959eb..585e2409 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -80,7 +80,7 @@ pub fn window_attributes( attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); - window_builder = window_builder.with_undecorated_shadow( + attributes = attributes.with_undecorated_shadow( settings.platform_specific.undecorated_shadow, ); } From 7cb12e3c3b62871953a35cee55499b52fddb9944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 5 Sep 2024 14:46:11 +0200 Subject: [PATCH 232/657] Flag `lazy` feature types directly Co-authored-by: JL710 <76447362+JL710@users.noreply.github.com> --- widget/src/lazy.rs | 1 + widget/src/lazy/component.rs | 1 + widget/src/lazy/helpers.rs | 5 +++-- widget/src/lazy/responsive.rs | 1 + widget/src/lib.rs | 3 --- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 4bcf8628..883a2f65 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -29,6 +29,7 @@ use std::hash::{Hash, Hasher as H}; use std::rc::Rc; /// A widget that only rebuilds its contents when necessary. +#[cfg(feature = "lazy")] #[allow(missing_debug_implementations)] pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> { dependency: Dependency, diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 1bf04195..1ec07e37 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -30,6 +30,7 @@ use std::rc::Rc; /// /// Additionally, a [`Component`] is capable of producing a `Message` to notify /// the parent application of any relevant interactions. +#[cfg(feature = "lazy")] pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { /// The internal state of this [`Component`]. type State: Default; diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 4d0776ca..862b251e 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -1,9 +1,10 @@ use crate::core::{self, Element, Size}; -use crate::lazy::component::{self, Component}; -use crate::lazy::{Lazy, Responsive}; +use crate::lazy::component; use std::hash::Hash; +pub use crate::lazy::{Component, Lazy, Responsive}; + /// Creates a new [`Lazy`] widget with the given data `Dependency` and a /// closure that can turn this data into a widget tree. #[cfg(feature = "lazy")] diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 2e24f2b3..dbf281f3 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -21,6 +21,7 @@ use std::ops::Deref; /// /// A [`Responsive`] widget will always try to fill all the available space of /// its parent. +#[cfg(feature = "lazy")] #[allow(missing_debug_implementations)] pub struct Responsive< 'a, diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 115a29e5..a68720d6 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -42,9 +42,6 @@ pub use helpers::*; #[cfg(feature = "lazy")] mod lazy; -#[cfg(feature = "lazy")] -pub use crate::lazy::{Component, Lazy, Responsive}; - #[cfg(feature = "lazy")] pub use crate::lazy::helpers::*; From 9426418adbaac40f584fe16b623521a3a21a1a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Thu, 5 Sep 2024 15:08:08 +0200 Subject: [PATCH 233/657] Deprecate the `component` widget --- examples/component/Cargo.toml | 10 --- examples/component/src/main.rs | 149 --------------------------------- widget/src/lazy.rs | 1 + widget/src/lazy/component.rs | 6 ++ widget/src/lazy/helpers.rs | 7 ++ 5 files changed, 14 insertions(+), 159 deletions(-) delete mode 100644 examples/component/Cargo.toml delete mode 100644 examples/component/src/main.rs diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml deleted file mode 100644 index 83b7b8a4..00000000 --- a/examples/component/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "component" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] -edition = "2021" -publish = false - -[dependencies] -iced.workspace = true -iced.features = ["debug", "lazy"] diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs deleted file mode 100644 index a5d2e508..00000000 --- a/examples/component/src/main.rs +++ /dev/null @@ -1,149 +0,0 @@ -use iced::widget::center; -use iced::Element; - -use numeric_input::numeric_input; - -pub fn main() -> iced::Result { - iced::run("Component - Iced", Component::update, Component::view) -} - -#[derive(Default)] -struct Component { - value: Option<u32>, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - NumericInputChanged(Option<u32>), -} - -impl Component { - fn update(&mut self, message: Message) { - match message { - Message::NumericInputChanged(value) => { - self.value = value; - } - } - } - - fn view(&self) -> Element<Message> { - center(numeric_input(self.value, Message::NumericInputChanged)) - .padding(20) - .into() - } -} - -mod numeric_input { - use iced::widget::{button, component, row, text, text_input, Component}; - use iced::{Center, Element, Fill, Length, Size}; - - pub struct NumericInput<Message> { - value: Option<u32>, - on_change: Box<dyn Fn(Option<u32>) -> Message>, - } - - pub fn numeric_input<Message>( - value: Option<u32>, - on_change: impl Fn(Option<u32>) -> Message + 'static, - ) -> NumericInput<Message> { - NumericInput::new(value, on_change) - } - - #[derive(Debug, Clone)] - pub enum Event { - InputChanged(String), - IncrementPressed, - DecrementPressed, - } - - impl<Message> NumericInput<Message> { - pub fn new( - value: Option<u32>, - on_change: impl Fn(Option<u32>) -> Message + 'static, - ) -> Self { - Self { - value, - on_change: Box::new(on_change), - } - } - } - - impl<Message, Theme> Component<Message, Theme> for NumericInput<Message> - where - Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, - { - type State = (); - type Event = Event; - - fn update( - &mut self, - _state: &mut Self::State, - event: Event, - ) -> Option<Message> { - match event { - Event::IncrementPressed => Some((self.on_change)(Some( - self.value.unwrap_or_default().saturating_add(1), - ))), - Event::DecrementPressed => Some((self.on_change)(Some( - self.value.unwrap_or_default().saturating_sub(1), - ))), - Event::InputChanged(value) => { - if value.is_empty() { - Some((self.on_change)(None)) - } else { - value - .parse() - .ok() - .map(Some) - .map(self.on_change.as_ref()) - } - } - } - } - - fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> { - let button = |label, on_press| { - button(text(label).width(Fill).height(Fill).center()) - .width(40) - .height(40) - .on_press(on_press) - }; - - row![ - button("-", Event::DecrementPressed), - text_input( - "Type a number", - self.value - .as_ref() - .map(u32::to_string) - .as_deref() - .unwrap_or(""), - ) - .on_input(Event::InputChanged) - .padding(10), - button("+", Event::IncrementPressed), - ] - .align_y(Center) - .spacing(10) - .into() - } - - fn size_hint(&self) -> Size<Length> { - Size { - width: Length::Fill, - height: Length::Shrink, - } - } - } - - impl<'a, Message, Theme> From<NumericInput<Message>> - for Element<'a, Message, Theme> - where - Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, - Message: 'a, - { - fn from(numeric_input: NumericInput<Message>) -> Self { - component(numeric_input) - } - } -} diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 883a2f65..221f9de3 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -4,6 +4,7 @@ pub(crate) mod helpers; pub mod component; pub mod responsive; +#[allow(deprecated)] pub use component::Component; pub use responsive::Responsive; diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 1ec07e37..659bc476 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -1,4 +1,5 @@ //! Build and reuse custom widgets using The Elm Architecture. +#![allow(deprecated)] use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; @@ -31,6 +32,11 @@ use std::rc::Rc; /// Additionally, a [`Component`] is capable of producing a `Message` to notify /// the parent application of any relevant interactions. #[cfg(feature = "lazy")] +#[deprecated( + since = "0.13.0", + note = "components introduce encapsulated state and hamper the use of a single source of truth. \ + Instead, leverage the Elm Architecture directly, or implement a custom widget" +)] pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { /// The internal state of this [`Component`]. type State: Default; diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 862b251e..52e690ff 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -3,6 +3,7 @@ use crate::lazy::component; use std::hash::Hash; +#[allow(deprecated)] pub use crate::lazy::{Component, Lazy, Responsive}; /// Creates a new [`Lazy`] widget with the given data `Dependency` and a @@ -22,6 +23,12 @@ where /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. #[cfg(feature = "lazy")] +#[deprecated( + since = "0.13.0", + note = "components introduce encapsulated state and hamper the use of a single source of truth. \ + Instead, leverage the Elm Architecture directly, or implement a custom widget" +)] +#[allow(deprecated)] pub fn component<'a, C, Message, Theme, Renderer>( component: C, ) -> Element<'a, Message, Theme, Renderer> From 827ba5b16c4acb1b63535898d4ef7df5ea2b8703 Mon Sep 17 00:00:00 2001 From: JL710 <76447362+JL710@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:42:35 +0200 Subject: [PATCH 234/657] Add `*_maybe` helper methods for `TextInput` --- widget/src/text_input.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 92047381..e129826c 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -137,6 +137,21 @@ where self } + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`], if `Some`. + /// + /// If `None`, the [`TextInput`] will be disabled. + pub fn on_input_maybe<F>(mut self, callback: Option<F>) -> Self + where + F: 'a + Fn(String) -> Message, + { + self.on_input = match callback { + Some(c) => Some(Box::new(c)), + None => None, + }; + self + } + /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. pub fn on_submit(mut self, message: Message) -> Self { @@ -144,6 +159,15 @@ where self } + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed, if `Some`. + /// + /// If `None` the [`TextInput`] nothing will happen. + pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self { + self.on_submit = on_submit; + self + } + /// Sets the message that should be produced when some text is pasted into /// the [`TextInput`]. pub fn on_paste( @@ -154,6 +178,21 @@ where self } + /// Sets the message that should be produced when some text is pasted into + /// the [`TextInput`], if `Some`. + /// + /// If `None` nothing will happen. + pub fn on_paste_maybe( + mut self, + on_paste: Option<impl Fn(String) -> Message + 'a>, + ) -> Self { + self.on_paste = match on_paste { + Some(func) => Some(Box::new(func)), + None => None, + }; + self + } + /// Sets the [`Font`] of the [`TextInput`]. /// /// [`Font`]: text::Renderer::Font From 09174d5a25aaea3dcdf177689ac23576ef81b377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sat, 7 Sep 2024 23:00:48 +0200 Subject: [PATCH 235/657] Simplify type signature of `TextInput` methods --- widget/src/text_input.rs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index e129826c..0a8e6690 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -129,11 +129,11 @@ where /// the [`TextInput`]. /// /// If this method is not called, the [`TextInput`] will be disabled. - pub fn on_input<F>(mut self, callback: F) -> Self - where - F: 'a + Fn(String) -> Message, - { - self.on_input = Some(Box::new(callback)); + pub fn on_input( + mut self, + on_input: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_input = Some(Box::new(on_input)); self } @@ -141,14 +141,11 @@ where /// the [`TextInput`], if `Some`. /// /// If `None`, the [`TextInput`] will be disabled. - pub fn on_input_maybe<F>(mut self, callback: Option<F>) -> Self - where - F: 'a + Fn(String) -> Message, - { - self.on_input = match callback { - Some(c) => Some(Box::new(c)), - None => None, - }; + pub fn on_input_maybe( + mut self, + on_input: Option<impl Fn(String) -> Message + 'a>, + ) -> Self { + self.on_input = on_input.map(|f| Box::new(f) as _); self } @@ -161,8 +158,6 @@ where /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed, if `Some`. - /// - /// If `None` the [`TextInput`] nothing will happen. pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self { self.on_submit = on_submit; self @@ -180,16 +175,11 @@ where /// Sets the message that should be produced when some text is pasted into /// the [`TextInput`], if `Some`. - /// - /// If `None` nothing will happen. pub fn on_paste_maybe( mut self, on_paste: Option<impl Fn(String) -> Message + 'a>, ) -> Self { - self.on_paste = match on_paste { - Some(func) => Some(Box::new(func)), - None => None, - }; + self.on_paste = on_paste.map(|f| Box::new(f) as _); self } From 502c5fdfbc2ef29a3ba2c645d78ab38c69f442a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Sun, 8 Sep 2024 16:00:22 +0200 Subject: [PATCH 236/657] Implement mouse wheel transactions for `scrollable` See https://wiki.mozilla.org/Gecko:Mouse_Wheel_Scrolling#Mouse_wheel_transaction Co-authored-by: Daniel Yoon <101683475+Koranir@users.noreply.github.com> --- widget/src/scrollable.rs | 43 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cf504eda..47953741 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -7,6 +7,7 @@ use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; use crate::core::widget::operation::{self, Operation}; @@ -470,6 +471,24 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); + if let Some(last_scrolled) = state.last_scrolled { + let clear_transaction = match event { + Event::Mouse( + mouse::Event::ButtonPressed(_) + | mouse::Event::ButtonReleased(_) + | mouse::Event::CursorLeft, + ) => true, + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + last_scrolled.elapsed() > Duration::from_millis(100) + } + _ => last_scrolled.elapsed() > Duration::from_millis(1500), + }; + + if clear_transaction { + state.last_scrolled = None; + } + } + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { match event { Event::Mouse(mouse::Event::CursorMoved { .. }) @@ -612,7 +631,11 @@ where } } - let mut event_status = { + let content_status = if state.last_scrolled.is_some() + && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. })) + { + event::Status::Ignored + } else { let cursor = match cursor_over_scrollable { Some(cursor_position) if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => @@ -660,10 +683,10 @@ where state.x_scroller_grabbed_at = None; state.y_scroller_grabbed_at = None; - return event_status; + return content_status; } - if let event::Status::Captured = event_status { + if let event::Status::Captured = content_status { return event::Status::Captured; } @@ -699,7 +722,7 @@ where state.scroll(delta, self.direction, bounds, content_bounds); - event_status = if notify_on_scroll( + if notify_on_scroll( state, &self.on_scroll, bounds, @@ -709,7 +732,7 @@ where event::Status::Captured } else { event::Status::Ignored - }; + } } Event::Touch(event) if state.scroll_area_touched_at.is_some() @@ -760,12 +783,10 @@ where _ => {} } - event_status = event::Status::Captured; + event::Status::Captured } - _ => {} + _ => event::Status::Ignored, } - - event_status } fn draw( @@ -1133,7 +1154,9 @@ fn notify_on_scroll<Message>( if let Some(on_scroll) = on_scroll { shell.publish(on_scroll(viewport)); } + state.last_notified = Some(viewport); + state.last_scrolled = Some(Instant::now()); true } @@ -1147,6 +1170,7 @@ struct State { x_scroller_grabbed_at: Option<f32>, keyboard_modifiers: keyboard::Modifiers, last_notified: Option<Viewport>, + last_scrolled: Option<Instant>, } impl Default for State { @@ -1159,6 +1183,7 @@ impl Default for State { x_scroller_grabbed_at: None, keyboard_modifiers: keyboard::Modifiers::default(), last_notified: None, + last_scrolled: None, } } } From 0a0ea30059b86ca70951aff535c078c1c4b2af0e Mon Sep 17 00:00:00 2001 From: Matt Woelfel <matt@woelfware.com> Date: Mon, 13 May 2024 21:16:19 -0500 Subject: [PATCH 237/657] Enable horizontal scrolling without shift modifier Fixes #2359. --- widget/src/scrollable.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 47953741..c2089340 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -706,15 +706,29 @@ where let delta = match delta { mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed - && state.keyboard_modifiers.shift() - { - Vector::new(y, x) - } else { - Vector::new(x, y) + let is_shift_pressed = state.keyboard_modifiers.shift(); + + // macOS automatically inverts the axes when Shift is pressed + let (x, y) = + if cfg!(target_os = "macos") && is_shift_pressed { + (y, x) + } else { + (x, y) + }; + + let is_vertical = match self.direction { + Direction::Vertical(_) => true, + Direction::Horizontal(_) => false, + Direction::Both { .. } => !is_shift_pressed, }; + let movement = if is_vertical { + Vector::new(x, y) + } else { + Vector::new(y, x) + }; + + // TODO: Configurable speed/friction (?) movement * 60.0 } mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), From 9edd805c0257cf360d7f9c0ee741c3508bdf8582 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <isaacryu@icloud.com> Date: Tue, 30 Apr 2024 11:49:50 -0400 Subject: [PATCH 238/657] Add `mouse::Button` to `mouse::Click` --- core/src/mouse/click.rs | 13 +++++++++++-- widget/src/text_editor.rs | 1 + widget/src/text_input.rs | 7 +++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 6f3844be..07a4db5a 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -1,4 +1,5 @@ //! Track mouse clicks. +use crate::mouse::Button; use crate::time::Instant; use crate::Point; @@ -6,6 +7,7 @@ use crate::Point; #[derive(Debug, Clone, Copy)] pub struct Click { kind: Kind, + button: Button, position: Point, time: Instant, } @@ -36,11 +38,17 @@ impl Kind { impl Click { /// Creates a new [`Click`] with the given position and previous last /// [`Click`]. - pub fn new(position: Point, previous: Option<Click>) -> Click { + pub fn new( + position: Point, + button: Button, + previous: Option<Click>, + ) -> Click { let time = Instant::now(); let kind = if let Some(previous) = previous { - if previous.is_consecutive(position, time) { + if previous.is_consecutive(position, time) + && button == previous.button + { previous.kind.next() } else { Kind::Single @@ -51,6 +59,7 @@ impl Click { Click { kind, + button, position, time, } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index d1aa4640..e0102656 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1056,6 +1056,7 @@ impl<Message> Update<Message> { let click = mouse::Click::new( cursor_position, + mouse::Button::Left, state.last_click, ); diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 0a8e6690..d5ede524 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -656,8 +656,11 @@ where cursor_position.x - text_bounds.x - alignment_offset }; - let click = - mouse::Click::new(cursor_position, state.last_click); + let click = mouse::Click::new( + cursor_position, + mouse::Button::Left, + state.last_click, + ); match click.kind() { click::Kind::Single => { From 2829c12d358b0d362cd180f83b242dcb592ca797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector0193@gmail.com> Date: Tue, 10 Sep 2024 16:26:15 +0200 Subject: [PATCH 239/657] Use `key_binding` in `editor` example Fixes #2573. --- examples/editor/src/main.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 5f12aec5..068782ba 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, text_editor, toggler, tooltip, }; -use iced::{Center, Element, Fill, Font, Subscription, Task, Theme}; +use iced::{Center, Element, Fill, Font, Task, Theme}; use std::ffi; use std::io; @@ -13,7 +13,6 @@ use std::sync::Arc; pub fn main() -> iced::Result { iced::application("Editor - Iced", Editor::update, Editor::view) - .subscription(Editor::subscription) .theme(Editor::theme) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .default_font(Font::MONOSPACE) @@ -137,15 +136,6 @@ impl Editor { } } - fn subscription(&self) -> Subscription<Message> { - keyboard::on_key_press(|key, modifiers| match key.as_ref() { - keyboard::Key::Character("s") if modifiers.command() => { - Some(Message::SaveFile) - } - _ => None, - }) - } - fn view(&self) -> Element<Message> { let controls = row![ action(new_icon(), "New file", Some(Message::NewFile)), @@ -214,7 +204,19 @@ impl Editor { .and_then(ffi::OsStr::to_str) .unwrap_or("rs"), self.theme, - ), + ) + .key_binding(|key_press| { + match key_press.key.as_ref() { + keyboard::Key::Character("s") + if key_press.modifiers.command() => + { + Some(text_editor::Binding::Custom( + Message::SaveFile, + )) + } + _ => text_editor::Binding::from_key_press(key_press), + } + }), status, ] .spacing(10) From 44235f0c0bcec1695a4504af55e3b00211db9f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 10 Sep 2024 18:09:13 +0200 Subject: [PATCH 240/657] Upgrade `upload-artifact` action in `build` workflow --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba1ab003..e7af3b03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Build todos binary run: cargo build --verbose --profile release-opt --package todos - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-unknown-linux-gnu path: target/release-opt/todos @@ -28,7 +28,7 @@ jobs: - name: Rename todos .deb package run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb - name: Archive todos .deb package - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-debian-linux-gnu path: target/debian/iced_todos-x86_64-debian-linux-gnu.deb @@ -48,7 +48,7 @@ jobs: - name: Build todos binary run: cargo build --verbose --profile release-opt --package todos - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-pc-windows-msvc path: target/release-opt/todos.exe @@ -65,7 +65,7 @@ jobs: - name: Open binary via double-click run: chmod +x target/release-opt/todos - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-x86_64-apple-darwin path: target/release-opt/todos @@ -80,14 +80,14 @@ jobs: - name: Build todos binary for Raspberry Pi 3/4 (64 bits) run: cross build --verbose --profile release-opt --package todos --target aarch64-unknown-linux-gnu - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-aarch64-unknown-linux-gnu path: target/aarch64-unknown-linux-gnu/release-opt/todos - name: Build todos binary for Raspberry Pi 2/3/4 (32 bits) run: cross build --verbose --profile release-opt --package todos --target armv7-unknown-linux-gnueabihf - name: Archive todos binary - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: todos-armv7-unknown-linux-gnueabihf path: target/armv7-unknown-linux-gnueabihf/release-opt/todos From 1a0bcdb2f68b63d8c01d823205d85f7d51bc88bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= <hector@hecrj.dev> Date: Tue, 10 Sep 2024 19:24:30 +0200 Subject: [PATCH 241/657] Fix `download_progress` and make it work on Wasm Co-authored-by: Skygrango <skygrango@gmail.com> --- examples/download_progress/Cargo.toml | 2 +- examples/download_progress/src/download.rs | 103 ++++++++------------- examples/download_progress/src/main.rs | 20 ++-- 3 files changed, 48 insertions(+), 77 deletions(-) diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index f78df529..61a1b257 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -12,4 +12,4 @@ iced.features = ["tokio"] [dependencies.reqwest] version = "0.12" default-features = false -features = ["rustls-tls"] +features = ["stream", "rustls-tls"] diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index bdf57290..a8e7b404 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -1,91 +1,62 @@ -use iced::futures; +use iced::futures::{SinkExt, Stream, StreamExt}; +use iced::stream::try_channel; use iced::Subscription; use std::hash::Hash; +use std::sync::Arc; // Just a little utility function pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>( id: I, url: T, -) -> iced::Subscription<(I, Progress)> { +) -> iced::Subscription<(I, Result<Progress, Error>)> { Subscription::run_with_id( id, - futures::stream::unfold(State::Ready(url.to_string()), move |state| { - use iced::futures::FutureExt; - - download(id, state).map(Some) - }), + download(url.to_string()).map(move |progress| (id, progress)), ) } -async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) { - match state { - State::Ready(url) => { - let response = reqwest::get(&url).await; +fn download(url: String) -> impl Stream<Item = Result<Progress, Error>> { + try_channel(1, move |mut output| async move { + let response = reqwest::get(&url).await?; + let total = response.content_length().ok_or(Error::NoContentLength)?; - match response { - Ok(response) => { - if let Some(total) = response.content_length() { - ( - (id, Progress::Started), - State::Downloading { - response, - total, - downloaded: 0, - }, - ) - } else { - ((id, Progress::Errored), State::Finished) - } - } - Err(_) => ((id, Progress::Errored), State::Finished), - } + let _ = output.send(Progress::Downloading { percent: 0.0 }).await; + + let mut byte_stream = response.bytes_stream(); + let mut downloaded = 0; + + while let Some(next_bytes) = byte_stream.next().await { + let bytes = next_bytes?; + downloaded += bytes.len(); + + let _ = output + .send(Progress::Downloading { + percent: 100.0 * downloaded as f32 / total as f32, + }) + .await; } - State::Downloading { - mut response, - total, - downloaded, - } => match response.chunk().await { - Ok(Some(chunk)) => { - let downloaded = downloaded + chunk.len() as u64; - let percentage = (downloaded as f32 / total as f32) * 100.0; + let _ = output.send(Progress::Finished).await; - ( - (id, Progress::Advanced(percentage)), - State::Downloading { - response, - total, - downloaded, - }, - ) - } - Ok(None) => ((id, Progress::Finished), State::Finished), - Err(_) => ((id, Progress::Errored), State::Finished), - }, - State::Finished => { - // We do not let the stream die, as it would start a - // new download repeatedly if the user is not careful - // in case of errors. - iced::futures::future::pending().await - } - } + Ok(()) + }) } #[derive(Debug, Clone)] pub enum Progress { - Started, - Advanced(f32), + Downloading { percent: f32 }, Finished, - Errored, } -pub enum State { - Ready(String), - Downloading { - response: reqwest::Response, - total: u64, - downloaded: u64, - }, - Finished, +#[derive(Debug, Clone)] +pub enum Error { + RequestFailed(Arc<reqwest::Error>), + NoContentLength, +} + +impl From<reqwest::Error> for Error { + fn from(error: reqwest::Error) -> Self { + Error::RequestFailed(Arc::new(error)) + } } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 667fb448..bcc01606 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -23,7 +23,7 @@ struct Example { pub enum Message { Add, Download(usize), - DownloadProgressed((usize, download::Progress)), + DownloadProgressed((usize, Result<download::Progress, download::Error>)), } impl Example { @@ -114,19 +114,19 @@ impl Download { } } - pub fn progress(&mut self, new_progress: download::Progress) { + pub fn progress( + &mut self, + new_progress: Result<download::Progress, download::Error>, + ) { if let State::Downloading { progress } = &mut self.state { match new_progress { - download::Progress::Started => { - *progress = 0.0; + Ok(download::Progress::Downloading { percent }) => { + *progress = percent; } - download::Progress::Advanced(percentage) => { - *progress = percentage; - } - download::Progress::Finished => { + Ok(download::Progress::Finished) => { self.state = State::Finished; } - download::Progress::Errored => { + Err(_error) => { self.state = State::Errored; } } @@ -136,7 +136,7 @@ impl Download { pub fn subscription(&self) -> Subscription<Message> { match self.state { State::Downloading { .. } => { - download::file(self.id, "https://speed.hetzner.de/100MB.bin?") + download::file(self.id, "https://huggingface.co/mattshumer/Reflection-Llama-3.1-70B/resolve/main/model-00001-of-00162.safetensors") .map(Message::DownloadProgressed) } _ => Subscription::none(), From 0053cc03f9cf92f4f476bdd52e5aca1ac630dfbf Mon Sep 17 00:00:00 2001 From: Skygrango <skygrango@gmail.com> Date: Fri, 3 May 2024 12:06:35 +0800 Subject: [PATCH 242/657] Add `index.html` to `download_progress` example --- examples/download_progress/index.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/download_progress/index.html diff --git a/examples/download_progress/index.html b/examples/download_progress/index.html new file mode 100644 index 00000000..c79e32c1 --- /dev/null +++ b/examples/download_progress/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en" style="height: 100%"> +<head> + <meta charset="utf-8" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Download_Progress - Iced + + + + + + From e102e89c6ade6fa5bb9a716a1d836a636dcf2918 Mon Sep 17 00:00:00 2001 From: lufte Date: Fri, 10 May 2024 18:50:10 -0300 Subject: [PATCH 243/657] Implement `scroll_by` operation for `scrollable` `scroll_by` allows scrolling an absolute offset that is applied to the current scrolling position. --- core/src/vector.rs | 11 +++ core/src/widget/operation.rs | 46 +++++++++++-- core/src/widget/operation/scrollable.rs | 45 +++++++++++++ widget/src/container.rs | 1 + widget/src/scrollable.rs | 90 ++++++++++++++++--------- 5 files changed, 157 insertions(+), 36 deletions(-) diff --git a/core/src/vector.rs b/core/src/vector.rs index 1380c3b3..ff848c4f 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -20,6 +20,17 @@ impl Vector { pub const ZERO: Self = Self::new(0.0, 0.0); } +impl std::ops::Neg for Vector +where + T: std::ops::Neg, +{ + type Output = Self; + + fn neg(self) -> Self::Output { + Self::new(-self.x, -self.y) + } +} + impl std::ops::Add for Vector where T: std::ops::Add, diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 4ee4b4a7..097c3601 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -38,6 +38,7 @@ pub trait Operation: Send { _state: &mut dyn Scrollable, _id: Option<&Id>, _bounds: Rectangle, + _content_bounds: Rectangle, _translation: Vector, ) { } @@ -76,9 +77,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: Vector, ) { - self.as_mut().scrollable(state, id, bounds, translation); + self.as_mut().scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { @@ -151,9 +159,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: Vector, ) { - self.operation.scrollable(state, id, bounds, translation); + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { @@ -222,9 +237,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: Vector, ) { - self.operation.scrollable(state, id, bounds, translation); + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn focusable( @@ -262,9 +284,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: Vector, ) { - self.operation.scrollable(state, id, bounds, translation); + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { @@ -341,9 +370,16 @@ where state: &mut dyn Scrollable, id: Option<&Id>, bounds: Rectangle, + content_bounds: Rectangle, translation: crate::Vector, ) { - self.operation.scrollable(state, id, bounds, translation); + self.operation.scrollable( + state, + id, + bounds, + content_bounds, + translation, + ); } fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index 12161255..c2fecf56 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -9,6 +9,14 @@ pub trait Scrollable { /// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis. fn scroll_to(&mut self, offset: AbsoluteOffset); + + /// Scroll the widget by the given [`AbsoluteOffset`] along the horizontal & vertical axis. + fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ); } /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to @@ -34,6 +42,7 @@ pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { state: &mut dyn Scrollable, id: Option<&Id>, _bounds: Rectangle, + _content_bounds: Rectangle, _translation: Vector, ) { if Some(&self.target) == id { @@ -68,6 +77,7 @@ pub fn scroll_to(target: Id, offset: AbsoluteOffset) -> impl Operation { state: &mut dyn Scrollable, id: Option<&Id>, _bounds: Rectangle, + _content_bounds: Rectangle, _translation: Vector, ) { if Some(&self.target) == id { @@ -79,6 +89,41 @@ pub fn scroll_to(target: Id, offset: AbsoluteOffset) -> impl Operation { ScrollTo { target, offset } } +/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by +/// the provided [`AbsoluteOffset`]. +pub fn scroll_by(target: Id, offset: AbsoluteOffset) -> impl Operation { + struct ScrollBy { + target: Id, + offset: AbsoluteOffset, + } + + impl Operation for ScrollBy { + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self); + } + + fn scrollable( + &mut self, + state: &mut dyn Scrollable, + id: Option<&Id>, + bounds: Rectangle, + content_bounds: Rectangle, + _translation: Vector, + ) { + if Some(&self.target) == id { + state.scroll_by(self.offset, bounds, content_bounds); + } + } + } + + ScrollBy { target, offset } +} + /// The amount of absolute offset in each direction of a [`Scrollable`]. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct AbsoluteOffset { diff --git a/widget/src/container.rs b/widget/src/container.rs index c3a66360..3b794099 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -459,6 +459,7 @@ pub fn visible_bounds(id: Id) -> Task> { _state: &mut dyn widget::operation::Scrollable, _id: Option<&widget::Id>, bounds: Rectangle, + _content_bounds: Rectangle, translation: Vector, ) { match self.scrollables.last() { diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c2089340..f8455392 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -243,6 +243,24 @@ impl Direction { Self::Horizontal(_) => None, } } + + fn align(&self, delta: Vector) -> Vector { + let horizontal_alignment = + self.horizontal().map(|p| p.alignment).unwrap_or_default(); + + let vertical_alignment = + self.vertical().map(|p| p.alignment).unwrap_or_default(); + + let align = |alignment: Anchor, delta: f32| match alignment { + Anchor::Start => delta, + Anchor::End => -delta, + }; + + Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ) + } } impl Default for Direction { @@ -430,6 +448,7 @@ where state, self.id.as_ref().map(|id| &id.0), bounds, + content_bounds, translation, ); @@ -729,12 +748,16 @@ where }; // TODO: Configurable speed/friction (?) - movement * 60.0 + -movement * 60.0 } mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), }; - state.scroll(delta, self.direction, bounds, content_bounds); + state.scroll( + self.direction.align(delta), + bounds, + content_bounds, + ); if notify_on_scroll( state, @@ -770,13 +793,12 @@ where }; let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, + scroll_box_touched_at.x - cursor_position.x, + scroll_box_touched_at.y - cursor_position.y, ); state.scroll( - delta, - self.direction, + self.direction.align(delta), bounds, content_bounds, ); @@ -1110,19 +1132,27 @@ impl From for widget::Id { } /// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage` along the x & y axis. +/// to the provided [`RelativeOffset`]. pub fn snap_to(id: Id, offset: RelativeOffset) -> Task { task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] -/// to the provided [`AbsoluteOffset`] along the x & y axis. +/// to the provided [`AbsoluteOffset`]. pub fn scroll_to(id: Id, offset: AbsoluteOffset) -> Task { task::effect(Action::widget(operation::scrollable::scroll_to( id.0, offset, ))) } +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] +/// by the provided [`AbsoluteOffset`]. +pub fn scroll_by(id: Id, offset: AbsoluteOffset) -> Task { + task::effect(Action::widget(operation::scrollable::scroll_by( + id.0, offset, + ))) +} + /// Returns [`true`] if the viewport actually changed. fn notify_on_scroll( state: &mut State, @@ -1210,6 +1240,15 @@ impl operation::Scrollable for State { fn scroll_to(&mut self, offset: AbsoluteOffset) { State::scroll_to(self, offset); } + + fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + State::scroll_by(self, offset, bounds, content_bounds); + } } #[derive(Debug, Clone, Copy)] @@ -1313,34 +1352,13 @@ impl State { pub fn scroll( &mut self, delta: Vector, - direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) { - let horizontal_alignment = direction - .horizontal() - .map(|p| p.alignment) - .unwrap_or_default(); - - let vertical_alignment = direction - .vertical() - .map(|p| p.alignment) - .unwrap_or_default(); - - let align = |alignment: Anchor, delta: f32| match alignment { - Anchor::Start => delta, - Anchor::End => -delta, - }; - - let delta = Vector::new( - align(horizontal_alignment, delta.x), - align(vertical_alignment, delta.y), - ); - if bounds.height < content_bounds.height { self.offset_y = Offset::Absolute( (self.offset_y.absolute(bounds.height, content_bounds.height) - - delta.y) + + delta.y) .clamp(0.0, content_bounds.height - bounds.height), ); } @@ -1348,7 +1366,7 @@ impl State { if bounds.width < content_bounds.width { self.offset_x = Offset::Absolute( (self.offset_x.absolute(bounds.width, content_bounds.width) - - delta.x) + + delta.x) .clamp(0.0, content_bounds.width - bounds.width), ); } @@ -1394,6 +1412,16 @@ impl State { self.offset_y = Offset::Absolute(offset.y.max(0.0)); } + /// Scroll by the provided [`AbsoluteOffset`]. + pub fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds); + } + /// Unsnaps the current scroll position, if snapped, given the bounds of the /// [`Scrollable`] and its contents. pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { From 716a11cc48b35a60f26c0b43bc5df1d174f35b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Sep 2024 22:38:30 +0200 Subject: [PATCH 244/657] Notify all `scrollable::Viewport` changes Co-authored-by: Daniel Yoon <101683475+Koranir@users.noreply.github.com> --- widget/src/scrollable.rs | 58 ++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index f8455392..af6a3945 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -12,6 +12,7 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ self, Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, @@ -526,7 +527,7 @@ where content_bounds, ); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -564,7 +565,7 @@ where state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -597,7 +598,7 @@ where content_bounds, ); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -635,7 +636,7 @@ where state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -759,7 +760,7 @@ where content_bounds, ); - if notify_on_scroll( + if notify_scroll( state, &self.on_scroll, bounds, @@ -807,7 +808,7 @@ where Some(cursor_position); // TODO: bubble up touch movements if not consumed. - let _ = notify_on_scroll( + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -821,6 +822,17 @@ where event::Status::Captured } + Event::Window(window::Event::RedrawRequested(_)) => { + let _ = notify_viewport( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event::Status::Ignored + } _ => event::Status::Ignored, } } @@ -1153,8 +1165,23 @@ pub fn scroll_by(id: Id, offset: AbsoluteOffset) -> Task { ))) } -/// Returns [`true`] if the viewport actually changed. -fn notify_on_scroll( +fn notify_scroll( + state: &mut State, + on_scroll: &Option Message + '_>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) -> bool { + if notify_viewport(state, on_scroll, bounds, content_bounds, shell) { + state.last_scrolled = Some(Instant::now()); + + true + } else { + false + } +} + +fn notify_viewport( state: &mut State, on_scroll: &Option Message + '_>>, bounds: Rectangle, @@ -1167,6 +1194,11 @@ fn notify_on_scroll( return false; } + let Some(on_scroll) = on_scroll else { + state.last_notified = None; + return false; + }; + let viewport = Viewport { offset_x: state.offset_x, offset_y: state.offset_y, @@ -1186,7 +1218,9 @@ fn notify_on_scroll( (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) }; - if unchanged(last_relative_offset.x, current_relative_offset.x) + if last_notified.bounds == bounds + && last_notified.content_bounds == content_bounds + && unchanged(last_relative_offset.x, current_relative_offset.x) && unchanged(last_relative_offset.y, current_relative_offset.y) && unchanged(last_absolute_offset.x, current_absolute_offset.x) && unchanged(last_absolute_offset.y, current_absolute_offset.y) @@ -1195,12 +1229,8 @@ fn notify_on_scroll( } } - if let Some(on_scroll) = on_scroll { - shell.publish(on_scroll(viewport)); - } - + shell.publish(on_scroll(viewport)); state.last_notified = Some(viewport); - state.last_scrolled = Some(Instant::now()); true } From ae58a40398bd02b07c1d7ca41681ae52b9ddde58 Mon Sep 17 00:00:00 2001 From: B0ney <40839054+B0ney@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:59:00 +0100 Subject: [PATCH 245/657] Render border above active progress for progress_bar widget. (#2443) * Render border above active progress for progress_bar widget. * Fix gap showing between border and background. * Include border style in active bar and make the border color transparent. --- widget/src/progress_bar.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 88d1850a..a10feea6 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -5,7 +5,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - self, Background, Element, Layout, Length, Rectangle, Size, Theme, Widget, + self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme, + Widget, }; use std::ops::RangeInclusive; @@ -151,7 +152,10 @@ where width: active_progress_width, ..bounds }, - border: border::rounded(style.border.radius), + border: Border { + color: Color::TRANSPARENT, + ..style.border + }, ..renderer::Quad::default() }, style.bar, From abd323181d613f1dc69b6cbe885dce556f427de2 Mon Sep 17 00:00:00 2001 From: B0ney <40839054+B0ney@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:30:37 +0100 Subject: [PATCH 246/657] Improve slider widget styling. (#2444) * Overhaul slider styling * Add `border` attribute to `Rail` * Replace `color` attribute with `background` for handle * Replace `colors` with `backgrounds` for the Rail. * code consistency * remove unused import --- widget/src/slider.rs | 36 +++++++++++++++++++---------------- widget/src/vertical_slider.rs | 12 ++++++------ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index e586684a..aebf68e2 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -9,8 +9,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Size, Theme, Widget, + self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Theme, Widget, }; use std::ops::RangeInclusive; @@ -408,10 +408,10 @@ where width: offset + handle_width / 2.0, height: style.rail.width, }, - border: border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.0, + style.rail.backgrounds.0, ); renderer.fill_quad( @@ -422,10 +422,10 @@ where width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border: border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.1, + style.rail.backgrounds.1, ); renderer.fill_quad( @@ -443,7 +443,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } @@ -524,12 +524,12 @@ impl Style { /// The appearance of a slider rail #[derive(Debug, Clone, Copy)] pub struct Rail { - /// The colors of the rail of the slider. - pub colors: (Color, Color), + /// The backgrounds of the rail of the slider. + pub backgrounds: (Background, Background), /// The width of the stroke of a slider rail. pub width: f32, - /// The border radius of the corners of the rail. - pub border_radius: border::Radius, + /// The border of the rail. + pub border: Border, } /// The appearance of the handle of a slider. @@ -537,8 +537,8 @@ pub struct Rail { pub struct Handle { /// The shape of the handle. pub shape: HandleShape, - /// The [`Color`] of the handle. - pub color: Color, + /// The [`Background`] of the handle. + pub background: Background, /// The border width of the handle. pub border_width: f32, /// The border [`Color`] of the handle. @@ -601,13 +601,17 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { rail: Rail { - colors: (color, palette.secondary.base.color), + backgrounds: (color.into(), palette.secondary.base.color.into()), width: 4.0, - border_radius: 2.0.into(), + border: Border { + radius: 2.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, }, handle: Handle { shape: HandleShape::Circle { radius: 7.0 }, - color, + background: color.into(), border_color: Color::TRANSPARENT, border_width: 0.0, }, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index f21b996c..03ec374c 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -5,7 +5,7 @@ pub use crate::slider::{ default, Catalog, Handle, HandleShape, Status, Style, StyleFn, }; -use crate::core::border::{self, Border}; +use crate::core::border::Border; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -413,10 +413,10 @@ where width: style.rail.width, height: offset + handle_width / 2.0, }, - border: border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.1, + style.rail.backgrounds.1, ); renderer.fill_quad( @@ -427,10 +427,10 @@ where width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border: border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.0, + style.rail.backgrounds.0, ); renderer.fill_quad( @@ -448,7 +448,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } From bf4796bbeb7a8274bf8eb32cfac51ad26de51681 Mon Sep 17 00:00:00 2001 From: Siliwolf Date: Sat, 25 May 2024 18:13:34 -0300 Subject: [PATCH 247/657] Add `on_scroll` handler to `mouse_area` widget --- widget/src/mouse_area.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 366335f4..d206322e 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,7 +1,5 @@ //! A container for capturing mouse events. -use iced_renderer::core::Point; - use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -10,7 +8,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, + Widget, }; /// Emit messages on mouse events. @@ -28,6 +27,7 @@ pub struct MouseArea< on_right_release: Option, on_middle_press: Option, on_middle_release: Option, + on_scroll: Option Message + 'a>>, on_enter: Option, on_move: Option Message>>, on_exit: Option, @@ -77,6 +77,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self } + /// The message to emit when scroll wheel is used + #[must_use] + pub fn on_scroll(mut self, on_scroll: F) -> Self + where + F: Fn(mouse::ScrollDelta) -> Message + 'static, + { + self.on_scroll = Some(Box::new(on_scroll)); + self + } + /// The message to emit when the mouse enters the area. #[must_use] pub fn on_enter(mut self, message: Message) -> Self { @@ -128,6 +138,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { on_right_release: None, on_middle_press: None, on_middle_release: None, + on_scroll: None, on_enter: None, on_move: None, on_exit: None, @@ -397,5 +408,13 @@ fn update( } } + if let Some(on_scroll) = widget.on_scroll.as_ref() { + if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + shell.publish(on_scroll(delta)); + + return event::Status::Captured; + } + } + event::Status::Ignored } From c711750be7ccca6a6852d0222169ebfea04ee864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Sep 2024 23:37:45 +0200 Subject: [PATCH 248/657] Use `cursor` changes to notify mouse events in `mouse_area` Fixes #2433. --- widget/src/mouse_area.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index d206322e..496afaa5 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,4 @@ //! A container for capturing mouse events. - use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -123,6 +122,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { #[derive(Default)] struct State { is_hovered: bool, + bounds: Rectangle, + cursor_position: Option, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -313,13 +314,17 @@ fn update( cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) = event - { - let state: &mut State = tree.state.downcast_mut(); + let state: &mut State = tree.state.downcast_mut(); + let cursor_position = cursor.position(); + let bounds = layout.bounds(); + + if state.cursor_position != cursor_position && state.bounds != bounds { let was_hovered = state.is_hovered; + state.is_hovered = cursor.is_over(layout.bounds()); + state.cursor_position = cursor_position; + state.bounds = bounds; match ( widget.on_enter.as_ref(), From 25e54a9acbd30799e994b6282700339e8aff7297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Sep 2024 23:41:07 +0200 Subject: [PATCH 249/657] Simplify signatures of `on_move` and `on_scroll` for `mouse_area` --- widget/src/mouse_area.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 496afaa5..d255ac99 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -28,7 +28,7 @@ pub struct MouseArea< on_middle_release: Option, on_scroll: Option Message + 'a>>, on_enter: Option, - on_move: Option Message>>, + on_move: Option Message + 'a>>, on_exit: Option, interaction: Option, } @@ -78,10 +78,10 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// The message to emit when scroll wheel is used #[must_use] - pub fn on_scroll(mut self, on_scroll: F) -> Self - where - F: Fn(mouse::ScrollDelta) -> Message + 'static, - { + pub fn on_scroll( + mut self, + on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a, + ) -> Self { self.on_scroll = Some(Box::new(on_scroll)); self } @@ -95,11 +95,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// The message to emit when the mouse moves in the area. #[must_use] - pub fn on_move(mut self, build_message: F) -> Self - where - F: Fn(Point) -> Message + 'static, - { - self.on_move = Some(Box::new(build_message)); + pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self { + self.on_move = Some(Box::new(on_move)); self } From ec39390c23cd46a115bb0528abdb2c5527f1272a Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz Date: Fri, 21 Jun 2024 10:41:17 +0100 Subject: [PATCH 250/657] Add stroke_rectangle This method should be able to leverage performance improvements in lyon's `tessellate_rectangle` over `tessellate_path`. --- graphics/src/geometry/frame.rs | 24 +++++++++++++++++++++ renderer/src/fallback.rs | 13 ++++++++++++ tiny_skia/src/geometry.rs | 25 ++++++++++++++++++++++ wgpu/src/geometry.rs | 38 ++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index b5f2f139..3dee7e75 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -65,6 +65,17 @@ where self.raw.stroke(path, stroke); } + /// Draws the stroke of an axis-aligned rectangle with the provided style + /// given its top-left corner coordinate and its `Size` on the [`Frame`] . + pub fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + self.raw.stroke_rectangle(top_left, size, stroke); + } + /// Draws the characters of the given [`Text`] on the [`Frame`], filling /// them with the given color. /// @@ -200,6 +211,12 @@ pub trait Backend: Sized { fn paste(&mut self, frame: Self); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>); + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ); fn fill(&mut self, path: &Path, fill: impl Into); fn fill_text(&mut self, text: impl Into); @@ -248,6 +265,13 @@ impl Backend for () { fn paste(&mut self, _frame: Self) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into>) {} + fn stroke_rectangle<'a>( + &mut self, + _top_left: Point, + _size: Size, + _stroke: impl Into>, + ) { + } fn fill(&mut self, _path: &Path, _fill: impl Into) {} fn fill_text(&mut self, _text: impl Into) {} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index fbd285db..8cb18bde 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -540,6 +540,19 @@ mod geometry { delegate!(self, frame, frame.stroke(path, stroke)); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + delegate!( + self, + frame, + frame.stroke_rectangle(top_left, size, stroke) + ); + } + fn fill_text(&mut self, text: impl Into) { delegate!(self, frame, frame.fill_text(text)); } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 659612d1..532a53cd 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -168,6 +168,31 @@ impl geometry::frame::Backend for Frame { }); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + let Some(path) = convert_path(&Path::rectangle(top_left, size)) + .and_then(|path| path.transform(self.transform)) + else { + return; + }; + + let stroke = stroke.into(); + let skia_stroke = into_stroke(&stroke); + + let mut paint = into_paint(stroke.style); + paint.shader.transform(self.transform); + + self.primitives.push(Primitive::Stroke { + path, + paint, + stroke: skia_stroke, + }); + } + fn fill_text(&mut self, text: impl Into) { let text = text.into(); diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index be65ba36..8e6f77d7 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -253,6 +253,44 @@ impl geometry::frame::Backend for Frame { .expect("Stroke path"); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); + + let size = + self.transforms.current.0.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + self.stroke_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Stroke rectangle"); + } + fn fill_text(&mut self, text: impl Into) { let text = text.into(); From fe8f41278dc922e12ffeb7a50bfb17a47b4bf956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Sep 2024 23:45:33 +0200 Subject: [PATCH 251/657] Leverage `stroke` for `stroke_rectangle` in `tiny-skia` backend --- tiny_skia/src/geometry.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 532a53cd..0d5fff62 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -174,23 +174,7 @@ impl geometry::frame::Backend for Frame { size: Size, stroke: impl Into>, ) { - let Some(path) = convert_path(&Path::rectangle(top_left, size)) - .and_then(|path| path.transform(self.transform)) - else { - return; - }; - - let stroke = stroke.into(); - let skia_stroke = into_stroke(&stroke); - - let mut paint = into_paint(stroke.style); - paint.shader.transform(self.transform); - - self.primitives.push(Primitive::Stroke { - path, - paint, - stroke: skia_stroke, - }); + self.stroke(&Path::rectangle(top_left, size), stroke); } fn fill_text(&mut self, text: impl Into) { From 190774258c3d2a32451b05738188973ae0393a42 Mon Sep 17 00:00:00 2001 From: Nadji Abidi Date: Sat, 22 Jun 2024 16:12:19 +0100 Subject: [PATCH 252/657] Add `override_redirect` for X11 windows This commit add the `override_redirect` boolean field to the `PlatformSpecific` struct for linux platform. This is a X11-specific flag allow bypassing window manager mapping for precise positioning of windows. --- core/src/window/settings/linux.rs | 6 ++++++ winit/src/conversion.rs | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/core/src/window/settings/linux.rs b/core/src/window/settings/linux.rs index 009b9d9e..0a1e11cd 100644 --- a/core/src/window/settings/linux.rs +++ b/core/src/window/settings/linux.rs @@ -8,4 +8,10 @@ pub struct PlatformSpecific { /// As a best practice, it is suggested to select an application id that match /// the basename of the application’s .desktop file. pub application_id: String, + + /// Whether bypass the window manager mapping for x11 windows + /// + /// This flag is particularly useful for creating UI elements that need precise + /// positioning and immediate display without window manager interference. + pub override_redirect: bool, } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 585e2409..aaaca1a9 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -105,10 +105,14 @@ pub fn window_attributes( { use winit::platform::x11::WindowAttributesExtX11; - attributes = attributes.with_name( - &settings.platform_specific.application_id, - &settings.platform_specific.application_id, - ); + attributes = attributes + .with_override_redirect( + settings.platform_specific.override_redirect, + ) + .with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); } #[cfg(feature = "wayland")] { From c741688b4c52dd2397880ca05b5f9a997d762246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 11 Sep 2024 00:17:16 +0200 Subject: [PATCH 253/657] Add disabled state and `on_toggle` handler to `Toggler` Co-authored-by: Your Name here only --- examples/editor/src/main.rs | 7 ++--- examples/styling/src/main.rs | 9 ++---- examples/tour/src/main.rs | 9 +++--- widget/src/helpers.rs | 3 +- widget/src/toggler.rs | 59 ++++++++++++++++++++++++++++-------- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 068782ba..c7d7eb26 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -150,11 +150,8 @@ impl Editor { self.is_dirty.then_some(Message::SaveFile) ), horizontal_space(), - toggler( - Some("Word Wrap"), - self.word_wrap, - Message::WordWrapToggled - ), + toggler(Some("Word Wrap"), self.word_wrap) + .on_toggle(Message::WordWrapToggled), pick_list( highlighter::Theme::ALL, Some(self.theme), diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index e19d5cf7..222ab79d 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -77,12 +77,9 @@ impl Styling { let checkbox = checkbox("Check me!", self.checkbox_value) .on_toggle(Message::CheckboxToggled); - let toggler = toggler( - Some("Toggle me!"), - self.toggler_value, - Message::TogglerToggled, - ) - .spacing(10); + let toggler = toggler(Some("Toggle me!"), self.toggler_value) + .on_toggle(Message::TogglerToggled) + .spacing(10); let content = column![ choose_theme, diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 0dd588fe..fad5f0b1 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -357,11 +357,10 @@ impl Tour { Self::container("Toggler") .push("A toggler is mostly used to enable or disable something.") .push( - Container::new(toggler( - Some("Toggle me to continue..."), - self.toggler, - Message::TogglerChanged, - )) + Container::new( + toggler(Some("Toggle me to continue..."), self.toggler) + .on_toggle(Message::TogglerChanged), + ) .padding([0, 40]), ) } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 349f02a6..e48ba328 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -769,13 +769,12 @@ where pub fn toggler<'a, Message, Theme, Renderer>( label: Option>, is_checked: bool, - f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where Theme: toggler::Catalog + 'a, Renderer: core::text::Renderer, { - Toggler::new(label, is_checked, f) + Toggler::new(label, is_checked) } /// Creates a new [`TextInput`]. diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 57e142e8..a6b2ae92 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -26,7 +26,8 @@ use crate::core::{ /// /// let is_toggled = true; /// -/// Toggler::new(Some("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); +/// Toggler::new(Some("Toggle me!"), is_toggled) +/// .on_toggle(Message::TogglerToggled); /// ``` #[allow(missing_debug_implementations)] pub struct Toggler< @@ -39,7 +40,7 @@ pub struct Toggler< Renderer: text::Renderer, { is_toggled: bool, - on_toggle: Box Message + 'a>, + on_toggle: Option Message + 'a>>, label: Option>, width: Length, size: f32, @@ -69,17 +70,13 @@ where /// * a function that will be called when the [`Toggler`] is toggled. It /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. - pub fn new( + pub fn new( label: Option>, is_toggled: bool, - f: F, - ) -> Self - where - F: 'a + Fn(bool) -> Message, - { + ) -> Self { Toggler { is_toggled, - on_toggle: Box::new(f), + on_toggle: None, label: label.map(text::IntoFragment::into_fragment), width: Length::Shrink, size: Self::DEFAULT_SIZE, @@ -94,6 +91,30 @@ where } } + /// Sets the message that should be produced when a user toggles + /// the [`Toggler`]. + /// + /// If this method is not called, the [`Toggler`] will be disabled. + pub fn on_toggle( + mut self, + on_toggle: impl Fn(bool) -> Message + 'a, + ) -> Self { + self.on_toggle = Some(Box::new(on_toggle)); + self + } + + /// Sets the message that should be produced when a user toggles + /// the [`Toggler`], if `Some`. + /// + /// If `None`, the [`Toggler`] will be disabled. + pub fn on_toggle_maybe( + mut self, + on_toggle: Option Message + 'a>, + ) -> Self { + self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _); + self + } + /// Sets the size of the [`Toggler`]. pub fn size(mut self, size: impl Into) -> Self { self.size = size.into().0; @@ -244,13 +265,17 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { + let Some(on_toggle) = &self.on_toggle else { + return event::Status::Ignored; + }; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { - shell.publish((self.on_toggle)(!self.is_toggled)); + shell.publish(on_toggle(!self.is_toggled)); event::Status::Captured } else { @@ -270,7 +295,11 @@ where _renderer: &Renderer, ) -> mouse::Interaction { if cursor.is_over(layout.bounds()) { - mouse::Interaction::Pointer + if self.on_toggle.is_some() { + mouse::Interaction::Pointer + } else { + mouse::Interaction::NotAllowed + } } else { mouse::Interaction::default() } @@ -314,7 +343,9 @@ where let bounds = toggler_layout.bounds(); let is_mouse_over = cursor.is_over(layout.bounds()); - let status = if is_mouse_over { + let status = if self.on_toggle.is_none() { + Status::Disabled + } else if is_mouse_over { Status::Hovered { is_toggled: self.is_toggled, } @@ -403,6 +434,8 @@ pub enum Status { /// Indicates whether the [`Toggler`] is toggled. is_toggled: bool, }, + /// The [`Toggler`] is disabled. + Disabled, } /// The appearance of a toggler. @@ -463,6 +496,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { palette.background.strong.color } } + Status::Disabled => palette.background.weak.color, }; let foreground = match status { @@ -483,6 +517,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { palette.background.weak.color } } + Status::Disabled => palette.background.base.color, }; Style { From 6e4970c01a9e42621a0ded340dcdccb4204ab5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 11 Sep 2024 00:20:23 +0200 Subject: [PATCH 254/657] Add `label` method to `Toggler` --- examples/editor/src/main.rs | 3 ++- examples/styling/src/main.rs | 3 ++- examples/tour/src/main.rs | 3 ++- widget/src/helpers.rs | 3 +-- widget/src/toggler.rs | 16 ++++++++++------ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index c7d7eb26..d55f9bdf 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -150,7 +150,8 @@ impl Editor { self.is_dirty.then_some(Message::SaveFile) ), horizontal_space(), - toggler(Some("Word Wrap"), self.word_wrap) + toggler(self.word_wrap) + .label("Word Wrap") .on_toggle(Message::WordWrapToggled), pick_list( highlighter::Theme::ALL, diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 222ab79d..534f5e32 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -77,7 +77,8 @@ impl Styling { let checkbox = checkbox("Check me!", self.checkbox_value) .on_toggle(Message::CheckboxToggled); - let toggler = toggler(Some("Toggle me!"), self.toggler_value) + let toggler = toggler(self.toggler_value) + .label("Toggle me!") .on_toggle(Message::TogglerToggled) .spacing(10); diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index fad5f0b1..d8c0b29a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -358,7 +358,8 @@ impl Tour { .push("A toggler is mostly used to enable or disable something.") .push( Container::new( - toggler(Some("Toggle me to continue..."), self.toggler) + toggler(self.toggler) + .label("Toggle me to continue...") .on_toggle(Message::TogglerChanged), ) .padding([0, 40]), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e48ba328..51978823 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -767,14 +767,13 @@ where /// /// [`Toggler`]: crate::Toggler pub fn toggler<'a, Message, Theme, Renderer>( - label: Option>, is_checked: bool, ) -> Toggler<'a, Message, Theme, Renderer> where Theme: toggler::Catalog + 'a, Renderer: core::text::Renderer, { - Toggler::new(label, is_checked) + Toggler::new(is_checked) } /// Creates a new [`TextInput`]. diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index a6b2ae92..1c425dc1 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -26,7 +26,8 @@ use crate::core::{ /// /// let is_toggled = true; /// -/// Toggler::new(Some("Toggle me!"), is_toggled) +/// Toggler::new(is_toggled) +/// .label("Toggle me!") /// .on_toggle(Message::TogglerToggled); /// ``` #[allow(missing_debug_implementations)] @@ -70,14 +71,11 @@ where /// * a function that will be called when the [`Toggler`] is toggled. It /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. - pub fn new( - label: Option>, - is_toggled: bool, - ) -> Self { + pub fn new(is_toggled: bool) -> Self { Toggler { is_toggled, on_toggle: None, - label: label.map(text::IntoFragment::into_fragment), + label: None, width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, @@ -91,6 +89,12 @@ where } } + /// Sets the label of the [`Toggler`]. + pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self { + self.label = Some(label.into_fragment()); + self + } + /// Sets the message that should be produced when a user toggles /// the [`Toggler`]. /// From 816facc2046035e669c39a69e13974b0dfb0379b Mon Sep 17 00:00:00 2001 From: Vlad-Stefan Harbuz Date: Sun, 30 Jun 2024 14:17:17 +0100 Subject: [PATCH 255/657] Add Color::from_hex --- core/src/color.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/core/src/color.rs b/core/src/color.rs index 4e79defb..d9271e83 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,5 +1,13 @@ use palette::rgb::{Srgb, Srgba}; +#[derive(Debug, thiserror::Error)] +/// Errors that can occur when constructing a [`Color`]. +pub enum ColorError { + #[error("The specified hex string is invalid. See supported formats.")] + /// The specified hex string is invalid. See supported formats. + InvalidHex, +} + /// A color in the `sRGB` color space. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Color { @@ -88,6 +96,52 @@ impl Color { } } + /// Creates a [`Color`] from a hex string. Supported formats are #rrggbb, #rrggbbaa, #rgb, + /// #rgba. The “#” is optional. Both uppercase and lowercase are supported. + pub fn from_hex(s: &str) -> Result { + let hex = s.strip_prefix('#').unwrap_or(s); + let n_chars = hex.len(); + + let get_channel = |from: usize, to: usize| { + let num = usize::from_str_radix(&hex[from..=to], 16) + .map_err(|_| ColorError::InvalidHex)? + as f32 + / 255.0; + // If we only got half a byte (one letter), expand it into a full byte (two letters) + Ok(if from == to { num + num * 16.0 } else { num }) + }; + + if n_chars == 3 { + Ok(Color::from_rgb( + get_channel(0, 0)?, + get_channel(1, 1)?, + get_channel(2, 2)?, + )) + } else if n_chars == 6 { + Ok(Color::from_rgb( + get_channel(0, 1)?, + get_channel(2, 3)?, + get_channel(4, 5)?, + )) + } else if n_chars == 4 { + Ok(Color::from_rgba( + get_channel(0, 0)?, + get_channel(1, 1)?, + get_channel(2, 2)?, + get_channel(3, 3)?, + )) + } else if n_chars == 8 { + Ok(Color::from_rgba( + get_channel(0, 1)?, + get_channel(2, 3)?, + get_channel(4, 5)?, + get_channel(6, 7)?, + )) + } else { + Err(ColorError::InvalidHex) + } + } + /// Creates a [`Color`] from its linear RGBA components. pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { // As described in: @@ -273,4 +327,19 @@ mod tests { assert_relative_eq!(result.b, 0.3); assert_relative_eq!(result.a, 1.0); } + + #[test] + fn from_hex() -> Result<(), ColorError> { + let tests = [ + ("#ff0000", [255, 0, 0, 255]), + ("00ff0080", [0, 255, 0, 128]), + ("#F80", [255, 136, 0, 255]), + ("#00f1", [0, 0, 255, 17]), + ]; + for (arg, expected) in tests { + assert_eq!(Color::from_hex(arg)?.into_rgba8(), expected); + } + assert!(Color::from_hex("invalid").is_err()); + Ok(()) + } } From 934667d263468e29b10137817f13ff4640fa46b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 11 Sep 2024 01:15:38 +0200 Subject: [PATCH 256/657] Improve flexibility of `color!` macro --- core/src/color.rs | 61 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index d9271e83..4f4b5e9b 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -232,34 +232,65 @@ impl From<[f32; 4]> for Color { /// /// ``` /// # use iced_core::{Color, color}; -/// assert_eq!(color!(0, 0, 0), Color::from_rgb(0., 0., 0.)); -/// assert_eq!(color!(0, 0, 0, 0.), Color::from_rgba(0., 0., 0., 0.)); -/// assert_eq!(color!(0xffffff), Color::from_rgb(1., 1., 1.)); -/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1., 1., 1., 0.)); +/// assert_eq!(color!(0, 0, 0), Color::BLACK); +/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT); +/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0)); +/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0)); +/// assert_eq!(color!(0x123), Color::from_rgba8(0x11, 0x22, 0x33, 1.0)); +/// assert_eq!(color!(0x123), color!(0x112233)); /// ``` #[macro_export] macro_rules! color { ($r:expr, $g:expr, $b:expr) => { color!($r, $g, $b, 1.0) }; - ($r:expr, $g:expr, $b:expr, $a:expr) => { - $crate::Color { - r: $r as f32 / 255.0, - g: $g as f32 / 255.0, - b: $b as f32 / 255.0, - a: $a, + ($r:expr, $g:expr, $b:expr, $a:expr) => {{ + let r = $r as f32 / 255.0; + let g = $g as f32 / 255.0; + let b = $b as f32 / 255.0; + + #[allow(clippy::manual_range_contains)] + { + debug_assert!( + r >= 0.0 && r <= 1.0, + "R channel must be in [0, 255] range." + ); + debug_assert!( + g >= 0.0 && g <= 1.0, + "G channel must be in [0, 255] range." + ); + debug_assert!( + b >= 0.0 && b <= 1.0, + "B channel must be in [0, 255] range." + ); } - }; + + $crate::Color { r, g, b, a: $a } + }}; ($hex:expr) => {{ color!($hex, 1.0) }}; ($hex:expr, $a:expr) => {{ let hex = $hex as u32; - let r = (hex & 0xff0000) >> 16; - let g = (hex & 0xff00) >> 8; - let b = (hex & 0xff); - color!(r, g, b, $a) + if hex <= 0xfff { + let r = (hex & 0xf00) >> 8; + let g = (hex & 0x0f0) >> 4; + let b = (hex & 0x00f); + + color!((r << 4 | r), (g << 4 | g), (b << 4 | b), $a) + } else { + debug_assert!( + hex <= 0xffffff, + "color! value must not exceed 0xffffff" + ); + + let r = (hex & 0xff0000) >> 16; + let g = (hex & 0xff00) >> 8; + let b = (hex & 0xff); + + color!(r, g, b, $a) + } }}; } From 523708b5b1665f647bbe377a459e7c26410f7af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 11 Sep 2024 01:20:47 +0200 Subject: [PATCH 257/657] Rename `Color::from_hex` to `Color::parse` --- core/src/color.rs | 94 ++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index 4f4b5e9b..89ec0e5b 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,13 +1,5 @@ use palette::rgb::{Srgb, Srgba}; -#[derive(Debug, thiserror::Error)] -/// Errors that can occur when constructing a [`Color`]. -pub enum ColorError { - #[error("The specified hex string is invalid. See supported formats.")] - /// The specified hex string is invalid. See supported formats. - InvalidHex, -} - /// A color in the `sRGB` color space. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Color { @@ -96,50 +88,46 @@ impl Color { } } - /// Creates a [`Color`] from a hex string. Supported formats are #rrggbb, #rrggbbaa, #rgb, - /// #rgba. The “#” is optional. Both uppercase and lowercase are supported. - pub fn from_hex(s: &str) -> Result { + /// Parses a [`Color`] from a hex string. + /// + /// Supported formats are #rrggbb, #rrggbbaa, #rgb, and #rgba. + /// The starting "#" is optional. Both uppercase and lowercase are supported. + pub fn parse(s: &str) -> Option { let hex = s.strip_prefix('#').unwrap_or(s); - let n_chars = hex.len(); - let get_channel = |from: usize, to: usize| { - let num = usize::from_str_radix(&hex[from..=to], 16) - .map_err(|_| ColorError::InvalidHex)? - as f32 - / 255.0; + let parse_channel = |from: usize, to: usize| { + let num = + usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0; + // If we only got half a byte (one letter), expand it into a full byte (two letters) - Ok(if from == to { num + num * 16.0 } else { num }) + Some(if from == to { num + num * 16.0 } else { num }) }; - if n_chars == 3 { - Ok(Color::from_rgb( - get_channel(0, 0)?, - get_channel(1, 1)?, - get_channel(2, 2)?, - )) - } else if n_chars == 6 { - Ok(Color::from_rgb( - get_channel(0, 1)?, - get_channel(2, 3)?, - get_channel(4, 5)?, - )) - } else if n_chars == 4 { - Ok(Color::from_rgba( - get_channel(0, 0)?, - get_channel(1, 1)?, - get_channel(2, 2)?, - get_channel(3, 3)?, - )) - } else if n_chars == 8 { - Ok(Color::from_rgba( - get_channel(0, 1)?, - get_channel(2, 3)?, - get_channel(4, 5)?, - get_channel(6, 7)?, - )) - } else { - Err(ColorError::InvalidHex) - } + Some(match hex.len() { + 3 => Color::from_rgb( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + ), + 4 => Color::from_rgba( + parse_channel(0, 0)?, + parse_channel(1, 1)?, + parse_channel(2, 2)?, + parse_channel(3, 3)?, + ), + 6 => Color::from_rgb( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + ), + 8 => Color::from_rgba( + parse_channel(0, 1)?, + parse_channel(2, 3)?, + parse_channel(4, 5)?, + parse_channel(6, 7)?, + ), + _ => None?, + }) } /// Creates a [`Color`] from its linear RGBA components. @@ -360,17 +348,21 @@ mod tests { } #[test] - fn from_hex() -> Result<(), ColorError> { + fn parse() { let tests = [ ("#ff0000", [255, 0, 0, 255]), ("00ff0080", [0, 255, 0, 128]), ("#F80", [255, 136, 0, 255]), ("#00f1", [0, 0, 255, 17]), ]; + for (arg, expected) in tests { - assert_eq!(Color::from_hex(arg)?.into_rgba8(), expected); + assert_eq!( + Color::parse(arg).expect("color must parse").into_rgba8(), + expected + ); } - assert!(Color::from_hex("invalid").is_err()); - Ok(()) + + assert!(Color::parse("invalid").is_none()); } } From 7901d4737c5c75467bc694e2fa37057fbf5ca111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 11 Sep 2024 01:28:03 +0200 Subject: [PATCH 258/657] Encourage use of `color!` macro in `Color::parse` docs --- core/src/color.rs | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index 89ec0e5b..46fe9ecd 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -88,10 +88,35 @@ impl Color { } } + /// Creates a [`Color`] from its linear RGBA components. + pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + // As described in: + // https://en.wikipedia.org/wiki/SRGB + fn gamma_component(u: f32) -> f32 { + if u < 0.0031308 { + 12.92 * u + } else { + 1.055 * u.powf(1.0 / 2.4) - 0.055 + } + } + + Self { + r: gamma_component(r), + g: gamma_component(g), + b: gamma_component(b), + a, + } + } + /// Parses a [`Color`] from a hex string. /// - /// Supported formats are #rrggbb, #rrggbbaa, #rgb, and #rgba. + /// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`. /// The starting "#" is optional. Both uppercase and lowercase are supported. + /// + /// If you have a static color string, using the [`color!`] macro should be preferred + /// since it leverages hexadecimal literal notation and arithmetic directly. + /// + /// [`color!`]: crate::color! pub fn parse(s: &str) -> Option { let hex = s.strip_prefix('#').unwrap_or(s); @@ -130,26 +155,6 @@ impl Color { }) } - /// Creates a [`Color`] from its linear RGBA components. - pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { - // As described in: - // https://en.wikipedia.org/wiki/SRGB - fn gamma_component(u: f32) -> f32 { - if u < 0.0031308 { - 12.92 * u - } else { - 1.055 * u.powf(1.0 / 2.4) - 0.055 - } - } - - Self { - r: gamma_component(r), - g: gamma_component(g), - b: gamma_component(b), - a, - } - } - /// Converts the [`Color`] into its RGBA8 equivalent. #[must_use] pub fn into_rgba8(self) -> [u8; 4] { From 7e89015e60d16dabe66dac8b168e212a7ac4b164 Mon Sep 17 00:00:00 2001 From: Gints Polis Date: Tue, 2 Jul 2024 23:59:24 +0300 Subject: [PATCH 259/657] Add `rounded_rectangle` to `geometry::Path` --- graphics/src/geometry/path.rs | 13 ++++- graphics/src/geometry/path/builder.rs | 68 ++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs index 3d8fc6fa..c4f51593 100644 --- a/graphics/src/geometry/path.rs +++ b/graphics/src/geometry/path.rs @@ -9,7 +9,8 @@ pub use builder::Builder; pub use lyon_path; -use iced_core::{Point, Size}; +use crate::core::border; +use crate::core::{Point, Size}; /// An immutable set of points that may or may not be connected. /// @@ -47,6 +48,16 @@ impl Path { Self::new(|p| p.rectangle(top_left, size)) } + /// Creates a new [`Path`] representing a rounded rectangle given its top-left + /// corner coordinate, its [`Size`] and [`border::Radius`]. + pub fn rounded_rectangle( + top_left: Point, + size: Size, + radius: border::Radius, + ) -> Self { + Self::new(|p| p.rounded_rectangle(top_left, size, radius)) + } + /// Creates a new [`Path`] representing a circle given its center /// coordinate and its radius. pub fn circle(center: Point, radius: f32) -> Self { diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs index 1ccd83f2..44410f6d 100644 --- a/graphics/src/geometry/path/builder.rs +++ b/graphics/src/geometry/path/builder.rs @@ -1,6 +1,7 @@ use crate::geometry::path::{arc, Arc, Path}; -use iced_core::{Point, Radians, Size}; +use crate::core::border; +use crate::core::{Point, Radians, Size}; use lyon_path::builder::{self, SvgPathBuilder}; use lyon_path::geom; @@ -160,6 +161,71 @@ impl Builder { self.close(); } + /// Adds a rounded rectangle to the [`Path`] given its top-left + /// corner coordinate its [`Size`] and [`border::Radius`]. + #[inline] + pub fn rounded_rectangle( + &mut self, + top_left: Point, + size: Size, + radius: border::Radius, + ) { + let min_size = (size.height / 2.0).min(size.width / 2.0); + let [top_left_corner, top_right_corner, bottom_right_corner, bottom_left_corner] = + radius.into(); + + self.move_to(Point::new( + top_left.x + min_size.min(top_left_corner), + top_left.y, + )); + self.line_to(Point::new( + top_left.x + size.width - min_size.min(top_right_corner), + top_left.y, + )); + self.arc_to( + Point::new(top_left.x + size.width, top_left.y), + Point::new( + top_left.x + size.width, + top_left.y + min_size.min(top_right_corner), + ), + min_size.min(top_right_corner), + ); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height - min_size.min(bottom_right_corner), + )); + self.arc_to( + Point::new(top_left.x + size.width, top_left.y + size.height), + Point::new( + top_left.x + size.width - min_size.min(bottom_right_corner), + top_left.y + size.height, + ), + min_size.min(bottom_right_corner), + ); + self.line_to(Point::new( + top_left.x + min_size.min(bottom_left_corner), + top_left.y + size.height, + )); + self.arc_to( + Point::new(top_left.x, top_left.y + size.height), + Point::new( + top_left.x, + top_left.y + size.height - min_size.min(bottom_left_corner), + ), + min_size.min(bottom_left_corner), + ); + self.line_to(Point::new( + top_left.x, + top_left.y + min_size.min(top_left_corner), + )); + self.arc_to( + Point::new(top_left.x, top_left.y), + Point::new(top_left.x + min_size.min(top_left_corner), top_left.y), + min_size.min(top_left_corner), + ); + self.close(); + } + /// Adds a circle to the [`Path`] given its center coordinate and its /// radius. #[inline] From ac1d98aa9b8b0bb0cf3bd6a3b89406d003403bfe Mon Sep 17 00:00:00 2001 From: Samson Date: Sun, 21 Jul 2024 09:29:24 -0500 Subject: [PATCH 260/657] feat: add width setter --- widget/src/text_editor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index e0102656..0029bc87 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -109,6 +109,12 @@ where self } + /// Sets the width of the [`TextEditor`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + /// Sets the message that should be produced when some action is performed in /// the [`TextEditor`]. /// From 4081e2b19257378ef6c454f2e3abe57f25a2f088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 01:08:23 +0200 Subject: [PATCH 261/657] Take `Into` in `TextEditor::width` Since a `Shrink` width would not make sense. --- widget/src/text_editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0029bc87..7e2a30f1 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -110,8 +110,8 @@ where } /// Sets the width of the [`TextEditor`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); + pub fn width(mut self, width: impl Into) -> Self { + self.width = Length::from(width.into()); self } From 7c7e94c8d1f9b843ab6d828f0976becbb447ba6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 01:10:36 +0200 Subject: [PATCH 262/657] Set `Limits::width` in `TextEditor` layout --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 7e2a30f1..5b565c39 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -504,7 +504,7 @@ where state.highlighter_settings = self.highlighter_settings.clone(); } - let limits = limits.height(self.height); + let limits = limits.width(self.width).height(self.height); internal.editor.update( limits.shrink(self.padding).max(), From 73ae2b4dbe536e4d056cc0d65385e7b586ec39dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 01:26:29 +0200 Subject: [PATCH 263/657] Fix priority of `Binding::Delete` in `text_editor` Co-authored-by: Trevor Campbell --- widget/src/text_editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 5b565c39..1df97962 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -981,7 +981,9 @@ impl Binding { keyboard::Key::Named(key::Named::Backspace) => { Some(Self::Backspace) } - keyboard::Key::Named(key::Named::Delete) => Some(Self::Delete), + keyboard::Key::Named(key::Named::Delete) if text.is_none() => { + Some(Self::Delete) + } keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), keyboard::Key::Character("c") if modifiers.command() => { Some(Self::Copy) From a497d123206162d47f5243b2dd28987535d846b5 Mon Sep 17 00:00:00 2001 From: JustSoup321 Date: Tue, 23 Jul 2024 20:39:14 -0400 Subject: [PATCH 264/657] Fix examples/multitouch dividing by zero --- examples/multitouch/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index a0105a8a..d5e5dffa 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -126,7 +126,7 @@ impl canvas::Program for Multitouch { let path = builder.build(); - let color_r = (10 % zone.0) as f32 / 20.0; + let color_r = (10 % (zone.0 + 1)) as f32 / 20.0; let color_g = (10 % (zone.0 + 8)) as f32 / 20.0; let color_b = (10 % (zone.0 + 3)) as f32 / 20.0; From c66355f289b1e389dc7de045d6ddfe75803302d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 01:48:15 +0200 Subject: [PATCH 265/657] Enter `Runtime` when calling `Program::subscription` --- winit/src/program.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 52d8eb5f..eef7e6c6 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -219,7 +219,7 @@ where } runtime.track(subscription::into_recipes( - program.subscription().map(Action::Output), + runtime.enter(|| program.subscription().map(Action::Output)), )); let (boot_sender, boot_receiver) = oneshot::channel(); @@ -1169,7 +1169,7 @@ fn update( } } - let subscription = program.subscription(); + let subscription = runtime.enter(|| program.subscription()); runtime.track(subscription::into_recipes(subscription.map(Action::Output))); } From e140c03b9b5e2f9e58a230a51646af5c3f9e85e5 Mon Sep 17 00:00:00 2001 From: may Date: Thu, 12 Sep 2024 23:54:36 +0200 Subject: [PATCH 266/657] Remove `Clone` bound for `graphics::Cache::clear` --- graphics/src/cache.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs index bbba79eb..7db80a01 100644 --- a/graphics/src/cache.rs +++ b/graphics/src/cache.rs @@ -1,6 +1,7 @@ //! Cache computations and efficiently reuse them. use std::cell::RefCell; use std::fmt; +use std::mem; use std::sync::atomic::{self, AtomicU64}; /// A simple cache that stores generated values to avoid recomputation. @@ -58,18 +59,18 @@ impl Cache { } /// Clears the [`Cache`]. - pub fn clear(&self) - where - T: Clone, - { - use std::ops::Deref; + pub fn clear(&self) { + let mut state = self.state.borrow_mut(); - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { current } => Some(current.clone()), + let previous = + mem::replace(&mut *state, State::Empty { previous: None }); + + let previous = match previous { + State::Empty { previous } => previous, + State::Filled { current } => Some(current), }; - *self.state.borrow_mut() = State::Empty { previous }; + *state = State::Empty { previous }; } } From cbe91d4a7cc6ef105747884425a3f12e00247856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 03:02:07 +0200 Subject: [PATCH 267/657] Add `physical_key` to `keyboard::Event` Co-authored-by: Exidex <16986685+Exidex@users.noreply.github.com> --- core/src/keyboard/event.rs | 4 + core/src/keyboard/key.rs | 533 +++++++++++++++++++++++++++++++++++++ winit/src/conversion.rs | 260 +++++++++++++++++- 3 files changed, 795 insertions(+), 2 deletions(-) diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 1eb42334..09625b18 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,3 +1,4 @@ +use crate::keyboard::key; use crate::keyboard::{Key, Location, Modifiers}; use crate::SmolStr; @@ -14,6 +15,9 @@ pub enum Event { /// The key pressed. key: Key, + /// The physical key pressed. + physical_key: key::Physical, + /// The location of the key. location: Location, diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index dbde5196..219452d7 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -742,3 +742,536 @@ pub enum Named { /// General-purpose function key. F35, } + +/// Code representing the location of a physical key. +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// exceptions: +/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and +/// "SuperRight" here. +/// - The key that the specification calls "Super" is reported as `Unidentified` here. +/// +/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[allow(missing_docs)] +#[non_exhaustive] +pub enum Code { + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or . + AltLeft, + /// Alt, Option, or . + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or . + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or + ControlLeft, + /// Control or + ControlRight, + /// Enter or . Labeled Return on Apple keyboards. + Enter, + /// The Windows, , Command, or other OS symbol key. + SuperLeft, + /// The Windows, , Command, or other OS symbol key. + SuperRight, + /// Shift or + ShiftLeft, + /// Shift or + ShiftRight, + /// (space) + Space, + /// Tab or + Tab, + /// Japanese: (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 + /// (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): (kana) + Lang1, + /// Korean: Hanja (hanja) + /// + /// Japanese (Mac keyboard): (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// . The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or + PageDown, + /// Page Up, PgUp, or + PageUp, + /// + ArrowDown, + /// + ArrowLeft, + /// + ArrowRight, + /// + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote + /// control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or + Escape, + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// Eject or . This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +/// Contains the platform-native physical key identifier +/// +/// The exact values vary from platform to platform (which is part of why this is a per-platform +/// enum), but the values are primarily tied to the key's physical location on the keyboard. +/// +/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native +/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we +/// haven't mapped for you yet, this lets you use use [`KeyCode`] to: +/// +/// - Correctly match key press and release events. +/// - On non-web platforms, support assigning keybinds to virtually any key through a UI. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum NativeCode { + /// An unidentified code. + Unidentified, + /// An Android "scancode". + Android(u32), + /// A macOS "scancode". + MacOS(u16), + /// A Windows "scancode". + Windows(u16), + /// An XKB "keycode". + Xkb(u32), +} + +/// Represents the location of a physical key. +/// +/// This type is a superset of [`KeyCode`], including an [`Unidentified`][Self::Unidentified] +/// variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Physical { + /// A known key code + Code(Code), + /// This variant is used when the key cannot be translated to a [`KeyCode`] + /// + /// The native keycode is provided (if available) so you're able to more reliably match + /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use + /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. + Unidentified(NativeCode), +} + +impl PartialEq for Physical { + #[inline] + fn eq(&self, rhs: &Code) -> bool { + match self { + Physical::Code(ref code) => code == rhs, + Physical::Unidentified(_) => false, + } + } +} + +impl PartialEq for Code { + #[inline] + fn eq(&self, rhs: &Physical) -> bool { + rhs == self + } +} + +impl PartialEq for Physical { + #[inline] + fn eq(&self, rhs: &NativeCode) -> bool { + match self { + Physical::Unidentified(ref code) => code == rhs, + Physical::Code(_) => false, + } + } +} + +impl PartialEq for NativeCode { + #[inline] + fn eq(&self, rhs: &Physical) -> bool { + rhs == self + } +} diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index aaaca1a9..0f336cc7 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -223,9 +223,13 @@ pub fn window_event( }.filter(|text| !text.as_str().chars().any(is_private_use)); let winit::event::KeyEvent { - state, location, .. + state, + location, + physical_key, + .. } = event; let key = key(logical_key); + let physical_key = self::physical_key(physical_key); let modifiers = self::modifiers(modifiers); let location = match location { @@ -245,6 +249,7 @@ pub fn window_event( winit::event::ElementState::Pressed => { keyboard::Event::KeyPressed { key, + physical_key, modifiers, location, text, @@ -510,7 +515,7 @@ pub fn touch_event( } } -/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced`] key code. +/// Converts a `Key` from [`winit`] to an [`iced`] key. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 @@ -839,6 +844,257 @@ pub fn key(key: winit::keyboard::Key) -> keyboard::Key { } } +/// Converts a `PhysicalKey` from [`winit`] to an [`iced`] physical key. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 +pub fn physical_key( + physical_key: winit::keyboard::PhysicalKey, +) -> keyboard::key::Physical { + match physical_key { + winit::keyboard::PhysicalKey::Code(code) => key_code(code) + .map(keyboard::key::Physical::Code) + .unwrap_or(keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + )), + winit::keyboard::PhysicalKey::Unidentified(code) => { + keyboard::key::Physical::Unidentified(native_key_code(code)) + } + } +} + +/// Converts a `KeyCode` from [`winit`] to an [`iced`] key code. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 +pub fn key_code( + key_code: winit::keyboard::KeyCode, +) -> Option { + use winit::keyboard::KeyCode; + + Some(match key_code { + KeyCode::Backquote => keyboard::key::Code::Backquote, + KeyCode::Backslash => keyboard::key::Code::Backslash, + KeyCode::BracketLeft => keyboard::key::Code::BracketLeft, + KeyCode::BracketRight => keyboard::key::Code::BracketRight, + KeyCode::Comma => keyboard::key::Code::Comma, + KeyCode::Digit0 => keyboard::key::Code::Digit0, + KeyCode::Digit1 => keyboard::key::Code::Digit1, + KeyCode::Digit2 => keyboard::key::Code::Digit2, + KeyCode::Digit3 => keyboard::key::Code::Digit3, + KeyCode::Digit4 => keyboard::key::Code::Digit4, + KeyCode::Digit5 => keyboard::key::Code::Digit5, + KeyCode::Digit6 => keyboard::key::Code::Digit6, + KeyCode::Digit7 => keyboard::key::Code::Digit7, + KeyCode::Digit8 => keyboard::key::Code::Digit8, + KeyCode::Digit9 => keyboard::key::Code::Digit9, + KeyCode::Equal => keyboard::key::Code::Equal, + KeyCode::IntlBackslash => keyboard::key::Code::IntlBackslash, + KeyCode::IntlRo => keyboard::key::Code::IntlRo, + KeyCode::IntlYen => keyboard::key::Code::IntlYen, + KeyCode::KeyA => keyboard::key::Code::KeyA, + KeyCode::KeyB => keyboard::key::Code::KeyB, + KeyCode::KeyC => keyboard::key::Code::KeyC, + KeyCode::KeyD => keyboard::key::Code::KeyD, + KeyCode::KeyE => keyboard::key::Code::KeyE, + KeyCode::KeyF => keyboard::key::Code::KeyF, + KeyCode::KeyG => keyboard::key::Code::KeyG, + KeyCode::KeyH => keyboard::key::Code::KeyH, + KeyCode::KeyI => keyboard::key::Code::KeyI, + KeyCode::KeyJ => keyboard::key::Code::KeyJ, + KeyCode::KeyK => keyboard::key::Code::KeyK, + KeyCode::KeyL => keyboard::key::Code::KeyL, + KeyCode::KeyM => keyboard::key::Code::KeyM, + KeyCode::KeyN => keyboard::key::Code::KeyN, + KeyCode::KeyO => keyboard::key::Code::KeyO, + KeyCode::KeyP => keyboard::key::Code::KeyP, + KeyCode::KeyQ => keyboard::key::Code::KeyQ, + KeyCode::KeyR => keyboard::key::Code::KeyR, + KeyCode::KeyS => keyboard::key::Code::KeyS, + KeyCode::KeyT => keyboard::key::Code::KeyT, + KeyCode::KeyU => keyboard::key::Code::KeyU, + KeyCode::KeyV => keyboard::key::Code::KeyV, + KeyCode::KeyW => keyboard::key::Code::KeyW, + KeyCode::KeyX => keyboard::key::Code::KeyX, + KeyCode::KeyY => keyboard::key::Code::KeyY, + KeyCode::KeyZ => keyboard::key::Code::KeyZ, + KeyCode::Minus => keyboard::key::Code::Minus, + KeyCode::Period => keyboard::key::Code::Period, + KeyCode::Quote => keyboard::key::Code::Quote, + KeyCode::Semicolon => keyboard::key::Code::Semicolon, + KeyCode::Slash => keyboard::key::Code::Slash, + KeyCode::AltLeft => keyboard::key::Code::AltLeft, + KeyCode::AltRight => keyboard::key::Code::AltRight, + KeyCode::Backspace => keyboard::key::Code::Backspace, + KeyCode::CapsLock => keyboard::key::Code::CapsLock, + KeyCode::ContextMenu => keyboard::key::Code::ContextMenu, + KeyCode::ControlLeft => keyboard::key::Code::ControlLeft, + KeyCode::ControlRight => keyboard::key::Code::ControlRight, + KeyCode::Enter => keyboard::key::Code::Enter, + KeyCode::SuperLeft => keyboard::key::Code::SuperLeft, + KeyCode::SuperRight => keyboard::key::Code::SuperRight, + KeyCode::ShiftLeft => keyboard::key::Code::ShiftLeft, + KeyCode::ShiftRight => keyboard::key::Code::ShiftRight, + KeyCode::Space => keyboard::key::Code::Space, + KeyCode::Tab => keyboard::key::Code::Tab, + KeyCode::Convert => keyboard::key::Code::Convert, + KeyCode::KanaMode => keyboard::key::Code::KanaMode, + KeyCode::Lang1 => keyboard::key::Code::Lang1, + KeyCode::Lang2 => keyboard::key::Code::Lang2, + KeyCode::Lang3 => keyboard::key::Code::Lang3, + KeyCode::Lang4 => keyboard::key::Code::Lang4, + KeyCode::Lang5 => keyboard::key::Code::Lang5, + KeyCode::NonConvert => keyboard::key::Code::NonConvert, + KeyCode::Delete => keyboard::key::Code::Delete, + KeyCode::End => keyboard::key::Code::End, + KeyCode::Help => keyboard::key::Code::Help, + KeyCode::Home => keyboard::key::Code::Home, + KeyCode::Insert => keyboard::key::Code::Insert, + KeyCode::PageDown => keyboard::key::Code::PageDown, + KeyCode::PageUp => keyboard::key::Code::PageUp, + KeyCode::ArrowDown => keyboard::key::Code::ArrowDown, + KeyCode::ArrowLeft => keyboard::key::Code::ArrowLeft, + KeyCode::ArrowRight => keyboard::key::Code::ArrowRight, + KeyCode::ArrowUp => keyboard::key::Code::ArrowUp, + KeyCode::NumLock => keyboard::key::Code::NumLock, + KeyCode::Numpad0 => keyboard::key::Code::Numpad0, + KeyCode::Numpad1 => keyboard::key::Code::Numpad1, + KeyCode::Numpad2 => keyboard::key::Code::Numpad2, + KeyCode::Numpad3 => keyboard::key::Code::Numpad3, + KeyCode::Numpad4 => keyboard::key::Code::Numpad4, + KeyCode::Numpad5 => keyboard::key::Code::Numpad5, + KeyCode::Numpad6 => keyboard::key::Code::Numpad6, + KeyCode::Numpad7 => keyboard::key::Code::Numpad7, + KeyCode::Numpad8 => keyboard::key::Code::Numpad8, + KeyCode::Numpad9 => keyboard::key::Code::Numpad9, + KeyCode::NumpadAdd => keyboard::key::Code::NumpadAdd, + KeyCode::NumpadBackspace => keyboard::key::Code::NumpadBackspace, + KeyCode::NumpadClear => keyboard::key::Code::NumpadClear, + KeyCode::NumpadClearEntry => keyboard::key::Code::NumpadClearEntry, + KeyCode::NumpadComma => keyboard::key::Code::NumpadComma, + KeyCode::NumpadDecimal => keyboard::key::Code::NumpadDecimal, + KeyCode::NumpadDivide => keyboard::key::Code::NumpadDivide, + KeyCode::NumpadEnter => keyboard::key::Code::NumpadEnter, + KeyCode::NumpadEqual => keyboard::key::Code::NumpadEqual, + KeyCode::NumpadHash => keyboard::key::Code::NumpadHash, + KeyCode::NumpadMemoryAdd => keyboard::key::Code::NumpadMemoryAdd, + KeyCode::NumpadMemoryClear => keyboard::key::Code::NumpadMemoryClear, + KeyCode::NumpadMemoryRecall => keyboard::key::Code::NumpadMemoryRecall, + KeyCode::NumpadMemoryStore => keyboard::key::Code::NumpadMemoryStore, + KeyCode::NumpadMemorySubtract => { + keyboard::key::Code::NumpadMemorySubtract + } + KeyCode::NumpadMultiply => keyboard::key::Code::NumpadMultiply, + KeyCode::NumpadParenLeft => keyboard::key::Code::NumpadParenLeft, + KeyCode::NumpadParenRight => keyboard::key::Code::NumpadParenRight, + KeyCode::NumpadStar => keyboard::key::Code::NumpadStar, + KeyCode::NumpadSubtract => keyboard::key::Code::NumpadSubtract, + KeyCode::Escape => keyboard::key::Code::Escape, + KeyCode::Fn => keyboard::key::Code::Fn, + KeyCode::FnLock => keyboard::key::Code::FnLock, + KeyCode::PrintScreen => keyboard::key::Code::PrintScreen, + KeyCode::ScrollLock => keyboard::key::Code::ScrollLock, + KeyCode::Pause => keyboard::key::Code::Pause, + KeyCode::BrowserBack => keyboard::key::Code::BrowserBack, + KeyCode::BrowserFavorites => keyboard::key::Code::BrowserFavorites, + KeyCode::BrowserForward => keyboard::key::Code::BrowserForward, + KeyCode::BrowserHome => keyboard::key::Code::BrowserHome, + KeyCode::BrowserRefresh => keyboard::key::Code::BrowserRefresh, + KeyCode::BrowserSearch => keyboard::key::Code::BrowserSearch, + KeyCode::BrowserStop => keyboard::key::Code::BrowserStop, + KeyCode::Eject => keyboard::key::Code::Eject, + KeyCode::LaunchApp1 => keyboard::key::Code::LaunchApp1, + KeyCode::LaunchApp2 => keyboard::key::Code::LaunchApp2, + KeyCode::LaunchMail => keyboard::key::Code::LaunchMail, + KeyCode::MediaPlayPause => keyboard::key::Code::MediaPlayPause, + KeyCode::MediaSelect => keyboard::key::Code::MediaSelect, + KeyCode::MediaStop => keyboard::key::Code::MediaStop, + KeyCode::MediaTrackNext => keyboard::key::Code::MediaTrackNext, + KeyCode::MediaTrackPrevious => keyboard::key::Code::MediaTrackPrevious, + KeyCode::Power => keyboard::key::Code::Power, + KeyCode::Sleep => keyboard::key::Code::Sleep, + KeyCode::AudioVolumeDown => keyboard::key::Code::AudioVolumeDown, + KeyCode::AudioVolumeMute => keyboard::key::Code::AudioVolumeMute, + KeyCode::AudioVolumeUp => keyboard::key::Code::AudioVolumeUp, + KeyCode::WakeUp => keyboard::key::Code::WakeUp, + KeyCode::Meta => keyboard::key::Code::Meta, + KeyCode::Hyper => keyboard::key::Code::Hyper, + KeyCode::Turbo => keyboard::key::Code::Turbo, + KeyCode::Abort => keyboard::key::Code::Abort, + KeyCode::Resume => keyboard::key::Code::Resume, + KeyCode::Suspend => keyboard::key::Code::Suspend, + KeyCode::Again => keyboard::key::Code::Again, + KeyCode::Copy => keyboard::key::Code::Copy, + KeyCode::Cut => keyboard::key::Code::Cut, + KeyCode::Find => keyboard::key::Code::Find, + KeyCode::Open => keyboard::key::Code::Open, + KeyCode::Paste => keyboard::key::Code::Paste, + KeyCode::Props => keyboard::key::Code::Props, + KeyCode::Select => keyboard::key::Code::Select, + KeyCode::Undo => keyboard::key::Code::Undo, + KeyCode::Hiragana => keyboard::key::Code::Hiragana, + KeyCode::Katakana => keyboard::key::Code::Katakana, + KeyCode::F1 => keyboard::key::Code::F1, + KeyCode::F2 => keyboard::key::Code::F2, + KeyCode::F3 => keyboard::key::Code::F3, + KeyCode::F4 => keyboard::key::Code::F4, + KeyCode::F5 => keyboard::key::Code::F5, + KeyCode::F6 => keyboard::key::Code::F6, + KeyCode::F7 => keyboard::key::Code::F7, + KeyCode::F8 => keyboard::key::Code::F8, + KeyCode::F9 => keyboard::key::Code::F9, + KeyCode::F10 => keyboard::key::Code::F10, + KeyCode::F11 => keyboard::key::Code::F11, + KeyCode::F12 => keyboard::key::Code::F12, + KeyCode::F13 => keyboard::key::Code::F13, + KeyCode::F14 => keyboard::key::Code::F14, + KeyCode::F15 => keyboard::key::Code::F15, + KeyCode::F16 => keyboard::key::Code::F16, + KeyCode::F17 => keyboard::key::Code::F17, + KeyCode::F18 => keyboard::key::Code::F18, + KeyCode::F19 => keyboard::key::Code::F19, + KeyCode::F20 => keyboard::key::Code::F20, + KeyCode::F21 => keyboard::key::Code::F21, + KeyCode::F22 => keyboard::key::Code::F22, + KeyCode::F23 => keyboard::key::Code::F23, + KeyCode::F24 => keyboard::key::Code::F24, + KeyCode::F25 => keyboard::key::Code::F25, + KeyCode::F26 => keyboard::key::Code::F26, + KeyCode::F27 => keyboard::key::Code::F27, + KeyCode::F28 => keyboard::key::Code::F28, + KeyCode::F29 => keyboard::key::Code::F29, + KeyCode::F30 => keyboard::key::Code::F30, + KeyCode::F31 => keyboard::key::Code::F31, + KeyCode::F32 => keyboard::key::Code::F32, + KeyCode::F33 => keyboard::key::Code::F33, + KeyCode::F34 => keyboard::key::Code::F34, + KeyCode::F35 => keyboard::key::Code::F35, + _ => None?, + }) +} + +/// Converts a `NativeKeyCode` from [`winit`] to an [`iced`] native key code. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 +pub fn native_key_code( + native_key_code: winit::keyboard::NativeKeyCode, +) -> keyboard::key::NativeCode { + use winit::keyboard::NativeKeyCode; + + match native_key_code { + NativeKeyCode::Unidentified => keyboard::key::NativeCode::Unidentified, + NativeKeyCode::Android(code) => { + keyboard::key::NativeCode::Android(code) + } + NativeKeyCode::MacOS(code) => keyboard::key::NativeCode::MacOS(code), + NativeKeyCode::Windows(code) => { + keyboard::key::NativeCode::Windows(code) + } + NativeKeyCode::Xkb(code) => keyboard::key::NativeCode::Xkb(code), + } +} + /// Converts some [`UserAttention`] into it's `winit` counterpart. /// /// [`UserAttention`]: window::UserAttention From 94c8b9639ca2d287c504345390585b80dc0d78cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 03:07:52 +0200 Subject: [PATCH 268/657] Add `modified_key` to `keyboard::Event` --- core/src/keyboard/event.rs | 3 +++ winit/src/conversion.rs | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 09625b18..26c45717 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -15,6 +15,9 @@ pub enum Event { /// The key pressed. key: Key, + /// The key pressed with all keyboard modifiers applied, except Ctrl. + modified_key: Key, + /// The physical key pressed. physical_key: key::Physical, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0f336cc7..13b0f15b 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -192,7 +192,7 @@ pub fn window_event( } }, WindowEvent::KeyboardInput { event, .. } => Some(Event::Keyboard({ - let logical_key = { + let key = { #[cfg(not(target_arch = "wasm32"))] { use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; @@ -202,7 +202,7 @@ pub fn window_event( #[cfg(target_arch = "wasm32")] { // TODO: Fix inconsistent API on Wasm - event.logical_key + event.logical_key.clone() } }; @@ -225,10 +225,13 @@ pub fn window_event( let winit::event::KeyEvent { state, location, + logical_key, physical_key, .. } = event; - let key = key(logical_key); + + let key = self::key(key); + let modified_key = self::key(logical_key); let physical_key = self::physical_key(physical_key); let modifiers = self::modifiers(modifiers); @@ -249,6 +252,7 @@ pub fn window_event( winit::event::ElementState::Pressed => { keyboard::Event::KeyPressed { key, + modified_key, physical_key, modifiers, location, From 4e9428bc5ad99c521858aae61cb6acc6d2c8bf52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 03:18:17 +0200 Subject: [PATCH 269/657] Fix broken doc links in `keyboard::key` --- core/src/keyboard/key.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index 219452d7..479d999b 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -1201,14 +1201,14 @@ pub enum Code { F35, } -/// Contains the platform-native physical key identifier +/// Contains the platform-native physical key identifier. /// /// The exact values vary from platform to platform (which is part of why this is a per-platform /// enum), but the values are primarily tied to the key's physical location on the keyboard. /// /// This enum is primarily used to store raw keycodes when Winit doesn't map a given native -/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we -/// haven't mapped for you yet, this lets you use use [`KeyCode`] to: +/// physical key identifier to a meaningful [`Code`] variant. In the presence of identifiers we +/// haven't mapped for you yet, this lets you use use [`Code`] to: /// /// - Correctly match key press and release events. /// - On non-web platforms, support assigning keybinds to virtually any key through a UI. @@ -1228,16 +1228,16 @@ pub enum NativeCode { /// Represents the location of a physical key. /// -/// This type is a superset of [`KeyCode`], including an [`Unidentified`][Self::Unidentified] +/// This type is a superset of [`Code`], including an [`Unidentified`][Self::Unidentified] /// variant. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Physical { /// A known key code Code(Code), - /// This variant is used when the key cannot be translated to a [`KeyCode`] + /// This variant is used when the key cannot be translated to a [`Code`] /// /// The native keycode is provided (if available) so you're able to more reliably match - /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use + /// key-press and key-release events by hashing the [`Physical`] key. It is also possible to use /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. Unidentified(NativeCode), } From 9d9ac0ff35f23c15bd2f4dd252855c8638e0f746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 16:38:38 +0200 Subject: [PATCH 270/657] Add `on_open` handler to `combo_box` widget Co-authored-by: Wail Abou --- widget/src/combo_box.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 62785b2c..a51701ca 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -41,6 +41,7 @@ pub struct ComboBox< selection: text_input::Value, on_selected: Box Message>, on_option_hovered: Option Message>>, + on_open: Option, on_close: Option, on_input: Option Message>>, menu_class: ::Class<'a>, @@ -77,6 +78,7 @@ where on_selected: Box::new(on_selected), on_option_hovered: None, on_input: None, + on_open: None, on_close: None, menu_class: ::default_menu(), padding: text_input::DEFAULT_PADDING, @@ -104,6 +106,13 @@ where self } + /// Sets the message that will be produced when the [`ComboBox`] is + /// opened. + pub fn on_open(mut self, message: Message) -> Self { + self.on_open = Some(message); + self + } + /// Sets the message that will be produced when the outside area /// of the [`ComboBox`] is pressed. pub fn on_close(mut self, message: Message) -> Self { @@ -632,15 +641,19 @@ where text_input_state.is_focused() }; - if started_focused && !is_focused && !published_message_to_shell { - if let Some(message) = self.on_close.take() { - shell.publish(message); - } - } - - // Focus changed, invalidate widget tree to force a fresh `view` if started_focused != is_focused { + // Focus changed, invalidate widget tree to force a fresh `view` shell.invalidate_widgets(); + + if !published_message_to_shell { + if is_focused { + if let Some(on_open) = self.on_open.take() { + shell.publish(on_open); + } + } else if let Some(on_close) = self.on_close.take() { + shell.publish(on_close); + } + } } event_status From 1cbedfaac702f16d098e2da2153e07e485780aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 16:55:40 +0200 Subject: [PATCH 271/657] Rename `ResizingDiagonal*` to `ResizingDiagonally*` --- core/src/mouse/interaction.rs | 4 ++-- winit/src/conversion.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index 92842668..9546c9c6 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -13,8 +13,8 @@ pub enum Interaction { Grabbing, ResizingHorizontally, ResizingVertically, - ResizingDiagonalUp, - ResizingDiagonalDown, + ResizingDiagonallyUp, + ResizingDiagonallyDown, NotAllowed, ZoomIn, ZoomOut, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index b974b3b9..d5eb9b97 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -423,10 +423,10 @@ pub fn mouse_interaction( winit::window::CursorIcon::EwResize } Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, - Interaction::ResizingDiagonalUp => { + Interaction::ResizingDiagonallyUp => { winit::window::CursorIcon::NeswResize } - Interaction::ResizingDiagonalDown => { + Interaction::ResizingDiagonallyDown => { winit::window::CursorIcon::NwseResize } Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed, From d4b9b4720fee91e4a0d737b46acc1618e6fc6c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 16:58:44 +0200 Subject: [PATCH 272/657] Add `Copy` and `Help` variants to `mouse::Interaction` --- core/src/mouse/interaction.rs | 2 ++ winit/src/conversion.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index 9546c9c6..aad6a3ea 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -20,4 +20,6 @@ pub enum Interaction { ZoomOut, Cell, Move, + Copy, + Help, } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index d5eb9b97..e762ae7c 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -434,6 +434,8 @@ pub fn mouse_interaction( Interaction::ZoomOut => winit::window::CursorIcon::ZoomOut, Interaction::Cell => winit::window::CursorIcon::Cell, Interaction::Move => winit::window::CursorIcon::Move, + Interaction::Copy => winit::window::CursorIcon::Copy, + Interaction::Help => winit::window::CursorIcon::Help, } } From 2367f7863a27fcfc00efeb07cd20f8bc16a2f951 Mon Sep 17 00:00:00 2001 From: dtoniolo Date: Fri, 23 Aug 2024 15:14:12 +0200 Subject: [PATCH 273/657] Document how the state of a `Component` can be managed --- widget/src/lazy/component.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 659bc476..2bdfa2c0 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -31,6 +31,19 @@ use std::rc::Rc; /// /// Additionally, a [`Component`] is capable of producing a `Message` to notify /// the parent application of any relevant interactions. +/// +/// # State +/// A component can store its state in one of two ways: either as data within the +/// implementor of the trait, or in a type [`State`][Component::State] that is managed +/// by the runtime and provided to the trait methods. These two approaches are not +/// mutually exclusive and have opposite pros and cons. +/// +/// For instance, if a piece of state is needed by multiple components that reside +/// in different branches of the tree, then it's more convenient to let a common +/// ancestor store it and pass it down. +/// +/// On the other hand, if a piece of state is only needed by the component itself, +/// you can store it as part of its internal [`State`][Component::State]. #[cfg(feature = "lazy")] #[deprecated( since = "0.13.0", From 0c502801e359706a182f01da1465c17b15fa6c67 Mon Sep 17 00:00:00 2001 From: Jovansonlee Cesar Date: Sat, 14 Sep 2024 05:43:00 +0800 Subject: [PATCH 274/657] Make rendering of svg that has text work out of the box (#2560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: load system fonts to usvg font_db, this will make rendering of text in svg that has it * feat: add an example that renders svg that has text on it * Initialize `fontdb` only once for `vector` images * Remove `svg_text` example * Set `fontdb` for `usvg::Options` in `tiny_skia::vector` --------- Co-authored-by: Héctor Ramón Jiménez --- tiny_skia/src/vector.rs | 31 +++++++++++++++++++++---------- wgpu/src/image/vector.rs | 30 +++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 8a15f47f..ea7de215 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -8,6 +8,7 @@ use tiny_skia::Transform; use std::cell::RefCell; use std::collections::hash_map; use std::fs; +use std::sync::Arc; #[derive(Debug)] pub struct Pipeline { @@ -68,6 +69,7 @@ struct Cache { tree_hits: FxHashSet, rasters: FxHashMap, raster_hits: FxHashSet, + fontdb: Option>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -81,23 +83,32 @@ impl Cache { fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { let id = handle.id(); + // TODO: Reuse `cosmic-text` font database + if self.fontdb.is_none() { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + + self.fontdb = Some(Arc::new(fontdb)); + } + + let options = usvg::Options { + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }; + if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { let svg = match handle.data() { Data::Path(path) => { fs::read_to_string(path).ok().and_then(|contents| { - usvg::Tree::from_str( - &contents, - &usvg::Options::default(), // TODO: Set usvg::Options::fontdb - ) - .ok() + usvg::Tree::from_str(&contents, &options).ok() }) } Data::Bytes(bytes) => { - usvg::Tree::from_data( - bytes, - &usvg::Options::default(), // TODO: Set usvg::Options::fontdb - ) - .ok() + usvg::Tree::from_data(bytes, &options).ok() } }; diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 74e9924d..e55ade38 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -6,6 +6,7 @@ use resvg::tiny_skia; use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use std::fs; +use std::sync::Arc; /// Entry in cache corresponding to an svg handle pub enum Svg { @@ -37,6 +38,7 @@ pub struct Cache { svg_hits: FxHashSet, rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, should_trim: bool, + fontdb: Option>, } type ColorFilter = Option<[u8; 4]>; @@ -48,23 +50,33 @@ impl Cache { return self.svgs.get(&handle.id()).unwrap(); } + // TODO: Reuse `cosmic-text` font database + if self.fontdb.is_none() { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + + self.fontdb = Some(Arc::new(fontdb)); + } + + let options = usvg::Options { + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }; + let svg = match handle.data() { svg::Data::Path(path) => fs::read_to_string(path) .ok() .and_then(|contents| { - usvg::Tree::from_str( - &contents, - &usvg::Options::default(), // TODO: Set usvg::Options::fontdb - ) - .ok() + usvg::Tree::from_str(&contents, &options).ok() }) .map(Svg::Loaded) .unwrap_or(Svg::NotFound), svg::Data::Bytes(bytes) => { - match usvg::Tree::from_data( - bytes, - &usvg::Options::default(), // TODO: Set usvg::Options::fontdb - ) { + match usvg::Tree::from_data(bytes, &options) { Ok(tree) => Svg::Loaded(tree), Err(_) => Svg::NotFound, } From cdf02ddda95a900215de8a9fa1681130795991bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 23:45:30 +0200 Subject: [PATCH 275/657] Enable `slider` scrolling only when `Ctrl` is pressed --- widget/src/slider.rs | 34 ++++++++++++++++++---------------- widget/src/vertical_slider.rs | 34 ++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 130c9bf3..5d0a363a 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -288,22 +288,6 @@ where }; match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if let Some(_) = cursor.position_over(layout.bounds()) { - let delta = match delta { - mouse::ScrollDelta::Lines { x: _, y } => y, - mouse::ScrollDelta::Pixels { x: _, y } => y, - }; - - if delta < 0.0 { - let _ = decrement(current_value).map(change); - } else { - let _ = increment(current_value).map(change); - } - - return event::Status::Captured; - } - } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = @@ -340,6 +324,24 @@ where return event::Status::Captured; } } + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if state.keyboard_modifiers.control() => + { + if let Some(_) = cursor.position_over(layout.bounds()) { + let delta = match delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; + + if delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { + let _ = increment(current_value).map(change); + } + + return event::Status::Captured; + } + } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if cursor.position_over(layout.bounds()).is_some() { match key { diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 5a3519f4..f8f4b4a3 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -291,22 +291,6 @@ where }; match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if let Some(_) = cursor.position_over(layout.bounds()) { - let delta = match delta { - mouse::ScrollDelta::Lines { x: _, y } => y, - mouse::ScrollDelta::Pixels { x: _, y } => y, - }; - - if delta < 0.0 { - let _ = decrement(current_value).map(change); - } else { - let _ = increment(current_value).map(change); - } - - return event::Status::Captured; - } - } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(cursor_position) = @@ -345,6 +329,24 @@ where return event::Status::Captured; } } + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if state.keyboard_modifiers.control() => + { + if let Some(_) = cursor.position_over(layout.bounds()) { + let delta = match delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; + + if delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { + let _ = increment(current_value).map(change); + } + + return event::Status::Captured; + } + } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if cursor.position_over(layout.bounds()).is_some() { match key { From 83041f6880a79fb1676fc1fd753474d189aa9b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Sep 2024 23:46:51 +0200 Subject: [PATCH 276/657] Use `mouse::Cursor::is_over` in `slider` --- widget/src/slider.rs | 4 ++-- widget/src/vertical_slider.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 5d0a363a..302cfae7 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -327,7 +327,7 @@ where Event::Mouse(mouse::Event::WheelScrolled { delta }) if state.keyboard_modifiers.control() => { - if let Some(_) = cursor.position_over(layout.bounds()) { + if cursor.is_over(layout.bounds()) { let delta = match delta { mouse::ScrollDelta::Lines { x: _, y } => y, mouse::ScrollDelta::Pixels { x: _, y } => y, @@ -343,7 +343,7 @@ where } } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { + if cursor.is_over(layout.bounds()) { match key { Key::Named(key::Named::ArrowUp) => { let _ = increment(current_value).map(change); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index f8f4b4a3..f02a490a 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -332,7 +332,7 @@ where Event::Mouse(mouse::Event::WheelScrolled { delta }) if state.keyboard_modifiers.control() => { - if let Some(_) = cursor.position_over(layout.bounds()) { + if cursor.is_over(layout.bounds()) { let delta = match delta { mouse::ScrollDelta::Lines { x: _, y } => y, mouse::ScrollDelta::Pixels { x: _, y } => y, @@ -348,7 +348,7 @@ where } } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { + if cursor.is_over(layout.bounds()) { match key { Key::Named(key::Named::ArrowUp) => { let _ = increment(current_value).map(change); From d46f6f92ce1f5f2776ab48f74dbd4439b1f17e9f Mon Sep 17 00:00:00 2001 From: Richard <30560559+derezzedex@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:15:15 -0300 Subject: [PATCH 277/657] Fix `wasm32` deployments not displaying anything (#2574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * reuse `canvas` element generated by dummy window * fix formatting * set `control_flow` to `Poll` in `resumed` this is mostly a fix for Chrome * Avoid blowing up memory when booting up on Wasm --------- Co-authored-by: Héctor Ramón Jiménez --- winit/Cargo.toml | 2 +- winit/src/program.rs | 67 +++++++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/winit/Cargo.toml b/winit/Cargo.toml index f5a47952..bd6feb00 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -44,5 +44,5 @@ winapi.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys.workspace = true -web-sys.features = ["Document", "Window"] +web-sys.features = ["Document", "Window", "HtmlCanvasElement"] wasm-bindgen-futures.workspace = true diff --git a/winit/src/program.rs b/winit/src/program.rs index eef7e6c6..7cfbd2d5 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -251,7 +251,7 @@ where #[cfg(target_arch = "wasm32")] is_booted: std::rc::Rc>, #[cfg(target_arch = "wasm32")] - queued_events: Vec>>, + canvas: Option, } struct BootConfig { @@ -276,7 +276,7 @@ where #[cfg(target_arch = "wasm32")] is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), #[cfg(target_arch = "wasm32")] - queued_events: Vec::new(), + canvas: None, }; impl winit::application::ApplicationHandler> @@ -307,6 +307,12 @@ where } }; + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + self.canvas = window.canvas(); + } + let finish_boot = async move { let mut compositor = C::new(graphics_settings, window.clone()).await?; @@ -340,6 +346,9 @@ where *is_booted.borrow_mut() = true; }); + + event_loop + .set_control_flow(winit::event_loop::ControlFlow::Poll); } } @@ -352,6 +361,11 @@ where return; } + #[cfg(target_arch = "wasm32")] + if !*self.is_booted.borrow() { + return; + } + self.process_event( event_loop, Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), @@ -430,6 +444,11 @@ where &mut self, event_loop: &winit::event_loop::ActiveEventLoop, ) { + #[cfg(target_arch = "wasm32")] + if !*self.is_booted.borrow() { + return; + } + self.process_event( event_loop, Event::EventLoopAwakened(winit::event::Event::AboutToWait), @@ -447,19 +466,6 @@ where event_loop: &winit::event_loop::ActiveEventLoop, event: Event>, ) { - #[cfg(target_arch = "wasm32")] - if !*self.is_booted.borrow() { - self.queued_events.push(event); - return; - } else if !self.queued_events.is_empty() { - let queued_events = std::mem::take(&mut self.queued_events); - - // This won't infinitely recurse, since we `mem::take` - for event in queued_events { - self.process_event(event_loop, event); - } - } - if event_loop.exiting() { return; } @@ -505,18 +511,27 @@ where let target = settings.platform_specific.target.clone(); - let window = event_loop - .create_window( - conversion::window_attributes( - settings, - &title, - monitor - .or(event_loop - .primary_monitor()), - self.id.clone(), - ) - .with_visible(false), + let window_attributes = + conversion::window_attributes( + settings, + &title, + monitor + .or(event_loop.primary_monitor()), + self.id.clone(), ) + .with_visible(false); + + #[cfg(target_arch = "wasm32")] + let window_attributes = { + use winit::platform::web::WindowAttributesExtWebSys; + window_attributes + .with_canvas(self.canvas.take()) + }; + + log::info!("Window attributes for id `{id:#?}`: {window_attributes:#?}"); + + let window = event_loop + .create_window(window_attributes) .expect("Create window"); #[cfg(target_arch = "wasm32")] From cadc0546519313d38c144833378c7376c0d016bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20Moravec?= <47433483+Jinderamarak@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:01:20 +0200 Subject: [PATCH 278/657] Disable `drag_and_drop` attribute for boot window --- winit/src/program.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 7cfbd2d5..8d1eec3a 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -296,14 +296,22 @@ where return; }; - let window = match event_loop.create_window( - winit::window::WindowAttributes::default().with_visible(false), - ) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; + let window = { + let attributes = winit::window::WindowAttributes::default(); + + #[cfg(target_os = "windows")] + let attributes = { + use winit::platform::windows::WindowAttributesExtWindows; + attributes.with_drag_and_drop(false) + }; + + match event_loop.create_window(attributes.with_visible(false)) { + Ok(window) => Arc::new(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; + } } }; From b3afe89be1e0a5fdeffb27c630970a2a912dd5b0 Mon Sep 17 00:00:00 2001 From: David Campbell Date: Sat, 14 Sep 2024 15:13:14 -0400 Subject: [PATCH 279/657] Add rust-version. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 52464e38..fbf6a01e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ members = [ version = "0.13.0-dev" authors = ["Héctor Ramón Jiménez "] edition = "2021" +rust-version = "1.75.0" license = "MIT" repository = "https://github.com/iced-rs/iced" homepage = "https://iced.rs" From 850b3d579d4f9a131c90a48f592ebdb88739f7dc Mon Sep 17 00:00:00 2001 From: David Campbell Date: Sat, 14 Sep 2024 15:22:08 -0400 Subject: [PATCH 280/657] Add to workspaces. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index fbf6a01e..296fef98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "iced" description = "A cross-platform GUI library inspired by Elm" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true From 547e509683007b9e0c149d847ac685f3aa770de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 17 Sep 2024 04:44:56 +0200 Subject: [PATCH 281/657] Implement a `changelog-generator` tool and example --- CHANGELOG.md | 5 +- examples/changelog/Cargo.toml | 23 ++ examples/changelog/fonts/changelog-icons.ttf | Bin 0 -> 5764 bytes examples/changelog/src/changelog.rs | 354 ++++++++++++++++++ examples/changelog/src/icon.rs | 10 + examples/changelog/src/main.rs | 368 +++++++++++++++++++ 6 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 examples/changelog/Cargo.toml create mode 100644 examples/changelog/fonts/changelog-icons.ttf create mode 100644 examples/changelog/src/changelog.rs create mode 100644 examples/changelog/src/icon.rs create mode 100644 examples/changelog/src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a69a7a..e8ac8d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280) -Many thanks to... +### Fixed +- Fix `block_on` in `iced_wgpu` hanging Wasm builds. [#2313](https://github.com/iced-rs/iced/pull/2313) +Many thanks to... +- @hecrj - @n1ght-hunter ## [0.12.1] - 2024-02-22 diff --git a/examples/changelog/Cargo.toml b/examples/changelog/Cargo.toml new file mode 100644 index 00000000..6f914bce --- /dev/null +++ b/examples/changelog/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "changelog" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["tokio", "markdown", "highlighter", "debug"] + +log.workspace = true +thiserror.workspace = true +tokio.features = ["fs", "process"] +tokio.workspace = true + +serde = "1" +webbrowser = "1" + +[dependencies.reqwest] +version = "0.12" +default-features = false +features = ["json", "rustls-tls"] diff --git a/examples/changelog/fonts/changelog-icons.ttf b/examples/changelog/fonts/changelog-icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a0f32553f25efedf1acc7a9cf56e65b21a170310 GIT binary patch literal 5764 zcmZQzWME+6XJ}wxW+-qE4s}xKR;^-SVEDtpz!2getZ!te?75eLfiZxAfgvF|H?iQQ z#_m-N42&BX7#ItZ%Ssd&z<~V>0|NtJT0wellHSiM1_t&!3=Dic>50V!3=9kc3=Hf? z7#J8h(sL@)v^f6fF)%QL^zF+?O-#|$2rpz{V1B^Bz+jer?V3mvv91M&MY*0}a<{-ui46F=1_qG*j3HomGBEftFoRh-49pB%3=E7R3@i*vU^Oft@fQsL|1&T^NCuGG!Tw_e zxf@~r$Yk+=y)6SK!Jz_59w5xX$iT$F@IQ-b4|5QMAcG`BhW7^EfCvW-P;|2uFfg+) zvM@6?FfcN*62c4&LJard z=9t1$HzNZ-gDF&;iGiKL1x{m<>!^8=H%q-CFkcRXC&sOr{?6R>t-hB=M@K~rkCa<7NLoval=xJiZk=` z6b$tY&}7_G^HPfvOHxxnHW!zr8 zgEK=uLjgl2LlHwJLpnnSLkWWdg9d{$gC>IlgAs!PgAs!Xg91YmLnVU(Lq1ehCPN-W zB0~;?0z)E0DMJZE23V$;L4m=6L4hHSA)g_Sp@booA%`J{A)i5yA(oE zF&Gdq(VZccArGuC5$y65xSxv|${5laj2KE7N*L0>zAFOzMuEW%Y(B!D3Je7dAh|4t zREA`R5(YhRXqGS(Fjz6@Gw94eD0Na+nssvCHmlQ%E~MQmX31}RqDz~e0Ky+ID7O3piRhX6w|NW~6ThNQ^Il*Gsl zjM|ZrP#;7pq-+p#R(6Wi-N34&;Ht2JMKv)+VFSB!K*R=iWv2}s%1+XXijf-_6LdGQ zfrEPkv$jIYM)o8p1&}1jyV4NngUAgG39iW-ShW-tHgGsAbSZRgVAS4dz^b}|MJ+HQ zAwW7YC^AAhQZZ6tgF|q{21adZP`E&yqPu}zX9K6R_9g};^bp+c6HKH zgeM$s9n^%x12KhBTX_S6^9B~x1O?YFa{ME|Dr5cvF=tbTwIfq7U z6cTdYz?kT?K~Ni%I&I_!5duuAP8%4-lod8Gs|G}D1m(jGEUHdjT?!y;1+)~E zH!vo`*wPSrkQOG@#0>&kijf=mo!vJGIJ+loU_y;7g$-Dva3ceQu(HbrM&}I-cAFR( z8Nu0JREJ?B1Cwh=#0DW{Ck59He9BH6c$GIWBseJ~Y?qK=WDsN!W^i(H0)>ExveO2} z#El{x+8esHrIjLeH;CzMWDwQX-5{>Bkr6~o=xk&H(ULkFnL)Ia&PEmxEv>VW6-3ME zY-9t`vN{{tLA0FCMh*}yud|U8L~H9P!@_?9TY`5;gtCI(2F6%WZqePKgCr>imn?)x z>LN+1!6l0zk_tKu8~C+x`f~$g;#P=PLH14N5v289}tN&PFB>t)jD$ z8APknJ2_kVsH) zR^Gsv;0!99L77kwlx7WdHt1__(AU!4V5p;@;I4opHkA`CltBeMUKP@ck-8gb&W8H1jnK=Qt;qXW1^#A3U;}v4g*w;D`qjVfl(W4+5?F*h;0Ga zTP}9W78^|%ML~Ww(^0TdaL3`p4GhkS7P=eEb@aq-bT?QKQo4cBIoU#YgQdNPZiyDs${V}60gR)d6m;szdNCrC;KC1+&?8wEXO3#fZ_?8OWhf z-@t>wQ)dH%C?wl&VgTiEVZ9AzTDlv&aEN>BY-C^*5#C^~rMtmLXM?4d?gn2_D7q>u z=qb2$DJQ~0%TEWCpf~tx>u&JZ*~Gx$rmednKxY#JBZv{GvxyNb5~Q<<5iAl6Qsb_z zyCDRm2E+&jsR1#cFg6kUB6c4x|puiU-;0s;#>r0c0nLkqELA#7F|!31TFJ)Ocv?Zb$*C0WnfR zYCw!MkQxvp9b_Mai?;5D43K?bRwl?kFe?jWADER5QpX5VmjhA{_wLzF590QCcxF(n)&*N9qPC0Afa6x4GfGd4jsuMV3iCmksz>, + added: Vec, + changed: Vec, + fixed: Vec, + removed: Vec, + authors: Vec, +} + +impl Changelog { + pub fn new() -> Self { + Self { + ids: Vec::new(), + added: Vec::new(), + changed: Vec::new(), + fixed: Vec::new(), + removed: Vec::new(), + authors: Vec::new(), + } + } + + pub async fn list() -> Result<(Self, Vec), Error> { + let mut changelog = Self::new(); + + { + let markdown = fs::read_to_string("CHANGELOG.md").await?; + + if let Some(unreleased) = markdown.split("\n## ").nth(1) { + let sections = unreleased.split("\n\n"); + + for section in sections { + if section.starts_with("Many thanks to...") { + for author in section.lines().skip(1) { + let author = author.trim_start_matches("- @"); + + if author.is_empty() { + continue; + } + + changelog.authors.push(author.to_owned()); + } + + continue; + } + + let Some((_, rest)) = section.split_once("### ") else { + continue; + }; + + let Some((name, rest)) = rest.split_once("\n") else { + continue; + }; + + let category = match name { + "Added" => Category::Added, + "Fixed" => Category::Fixed, + "Changed" => Category::Changed, + "Removed" => Category::Removed, + _ => continue, + }; + + for entry in rest.lines() { + let Some((_, id)) = entry.split_once('#') else { + continue; + }; + + let Some((id, _)) = id.split_once(']') else { + continue; + }; + + let Ok(id): Result = id.parse() else { + continue; + }; + + changelog.ids.push(id); + + let target = match category { + Category::Added => &mut changelog.added, + Category::Changed => &mut changelog.added, + Category::Fixed => &mut changelog.fixed, + Category::Removed => &mut changelog.removed, + }; + + target.push(entry.to_owned()); + } + } + } + } + + let mut candidates = Candidate::list().await?; + + for reviewed_entry in changelog.entries() { + candidates.retain(|candidate| candidate.id != reviewed_entry); + } + + Ok((changelog, candidates)) + } + + pub fn len(&self) -> usize { + self.ids.len() + } + + pub fn entries(&self) -> impl Iterator + '_ { + self.ids.iter().copied() + } + + pub fn push(&mut self, entry: Entry) { + self.ids.push(entry.id); + + let item = format!( + "- {title}. [#{id}](https://github.com/iced-rs/iced/pull/{id})", + title = entry.title, + id = entry.id + ); + + let target = match entry.category { + Category::Added => &mut self.added, + Category::Changed => &mut self.added, + Category::Fixed => &mut self.fixed, + Category::Removed => &mut self.removed, + }; + + target.push(item); + + if !self.authors.contains(&entry.author) { + self.authors.push(entry.author); + self.authors.sort_by_key(|author| author.to_lowercase()); + } + } +} + +impl fmt::Display for Changelog { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn section(category: Category, entries: &[String]) -> String { + if entries.is_empty() { + return String::new(); + } + + format!("### {category}\n{list}\n", list = entries.join("\n")) + } + + fn thank_you<'a>(authors: impl IntoIterator) -> String { + let mut list = String::new(); + + for author in authors { + list.push_str(&format!("- @{author}\n")); + } + + format!("Many thanks to...\n{list}") + } + + let changelog = [ + section(Category::Added, &self.added), + section(Category::Changed, &self.changed), + section(Category::Fixed, &self.fixed), + section(Category::Removed, &self.removed), + thank_you(self.authors.iter().map(String::as_str)), + ] + .into_iter() + .filter(|section| !section.is_empty()) + .collect::>() + .join("\n"); + + f.write_str(&changelog) + } +} + +#[derive(Debug, Clone)] +pub struct Entry { + pub id: u64, + pub title: String, + pub category: Category, + pub author: String, +} + +impl Entry { + pub fn new( + title: &str, + category: Category, + pull_request: &PullRequest, + ) -> Option { + let title = title.strip_suffix(".").unwrap_or(title); + + if title.is_empty() { + return None; + }; + + Some(Self { + id: pull_request.id, + title: title.to_owned(), + category, + author: pull_request.author.clone(), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Category { + Added, + Changed, + Fixed, + Removed, +} + +impl Category { + pub const ALL: &'static [Self] = + &[Self::Added, Self::Changed, Self::Fixed, Self::Removed]; + + pub fn guess(label: &str) -> Option { + Some(match label { + "feature" | "addition" => Self::Added, + "change" => Self::Changed, + "bug" | "fix" => Self::Fixed, + _ => None?, + }) + } +} + +impl fmt::Display for Category { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Category::Added => "Added", + Category::Changed => "Changed", + Category::Fixed => "Fixed", + Category::Removed => "Removed", + }) + } +} + +#[derive(Debug, Clone)] +pub struct Candidate { + pub id: u64, +} + +#[derive(Debug, Clone)] +pub struct PullRequest { + pub id: u64, + pub title: String, + pub description: String, + pub labels: Vec, + pub author: String, +} + +impl Candidate { + pub async fn list() -> Result, Error> { + let output = process::Command::new("git") + .args([ + "log", + "--oneline", + "--grep", + "#[0-9]*", + "origin/latest..HEAD", + ]) + .output() + .await?; + + let log = String::from_utf8_lossy(&output.stdout); + + Ok(log + .lines() + .filter(|title| !title.is_empty()) + .filter_map(|title| { + let (_, pull_request) = title.split_once("#")?; + let (pull_request, _) = pull_request.split_once([')', ' '])?; + + Some(Candidate { + id: pull_request.parse().ok()?, + }) + }) + .collect()) + } + + pub async fn fetch(self) -> Result { + let request = reqwest::Client::new() + .request( + reqwest::Method::GET, + format!( + "https://api.github.com/repos/iced-rs/iced/pulls/{}", + self.id + ), + ) + .header("User-Agent", "iced changelog generator") + .header( + "Authorization", + format!( + "Bearer {}", + env::var("GITHUB_TOKEN") + .map_err(|_| Error::GitHubTokenNotFound)? + ), + ); + + #[derive(Deserialize)] + struct Schema { + title: String, + body: String, + user: User, + labels: Vec

{ window: self.window, } } + + /// Sets the executor of the [`Application`]. + pub fn executor( + self, + ) -> Application< + impl Program, + > + where + E: Executor, + { + Application { + raw: program::with_executor::(self.raw), + settings: self.settings, + window: self.window, + } + } } /// The title logic of some [`Application`]. diff --git a/src/program.rs b/src/program.rs index 2b697fbe..68efab88 100644 --- a/src/program.rs +++ b/src/program.rs @@ -550,6 +550,80 @@ pub fn with_scale_factor( } } +pub fn with_executor( + program: P, +) -> impl Program { + use std::marker::PhantomData; + + struct WithExecutor { + program: P, + executor: PhantomData, + } + + impl Program for WithExecutor + where + E: Executor, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = E; + + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state, window) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } + } + + WithExecutor { + program, + executor: PhantomData::, + } +} + /// The renderer of some [`Program`]. pub trait Renderer: text::Renderer + compositor::Default {} From 1448c5bfa5d0977e54670bb8c640ba186bb13167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 20:30:14 +0200 Subject: [PATCH 295/657] Implement some `From` traits for `text_input::Id` --- core/src/window/id.rs | 8 ++++++- examples/multi_window/src/main.rs | 6 ++--- examples/todos/Cargo.toml | 1 - examples/todos/src/main.rs | 7 ++---- widget/src/text_input.rs | 39 +++++++++++++++++++++---------- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 1a75fa27..5d5a817e 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -1,5 +1,5 @@ +use std::fmt; use std::hash::Hash; - use std::sync::atomic::{self, AtomicU64}; /// The id of the window. @@ -14,3 +14,9 @@ impl Id { Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) } } + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index ab09116e..b43a627a 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -25,7 +25,6 @@ struct Window { scale_input: String, current_scale: f64, theme: Theme, - input_id: iced::widget::text_input::Id, } #[derive(Debug, Clone)] @@ -86,7 +85,7 @@ impl Example { } Message::WindowOpened(id) => { let window = Window::new(self.windows.len() + 1); - let focus_input = text_input::focus(window.input_id.clone()); + let focus_input = text_input::focus(format!("input-{id}")); self.windows.insert(id, window); @@ -163,7 +162,6 @@ impl Window { scale_input: "1.0".to_string(), current_scale: 1.0, theme: Theme::ALL[count % Theme::ALL.len()].clone(), - input_id: text_input::Id::unique(), } } @@ -182,7 +180,7 @@ impl Window { text("Window title:"), text_input("Window Title", &self.title) .on_input(move |msg| { Message::TitleChanged(id, msg) }) - .id(self.input_id.clone()) + .id(format!("input-{id}")) ]; let new_window_button = diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 3c62bfbc..0d72be86 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -9,7 +9,6 @@ publish = false iced.workspace = true iced.features = ["async-std", "debug"] -once_cell.workspace = true serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.0", features = ["v4", "fast-rng", "serde"] } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index a5f7b36a..25e3ead2 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -6,12 +6,9 @@ use iced::widget::{ use iced::window; use iced::{Center, Element, Fill, Font, Subscription, Task as Command}; -use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use uuid::Uuid; -static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); - pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); @@ -85,7 +82,7 @@ impl Todos { _ => {} } - text_input::focus(INPUT_ID.clone()) + text_input::focus("new-task") } Todos::Loaded(state) => { let mut saved = false; @@ -198,7 +195,7 @@ impl Todos { .align_x(Center); let input = text_input("What needs to be done?", input_value) - .id(INPUT_ID.clone()) + .id("new-task") .on_input(Message::InputChanged) .on_submit(Message::CreateTask) .padding(15) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index d5ede524..3032dd13 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -114,8 +114,8 @@ where } /// Sets the [`Id`] of the [`TextInput`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); self } @@ -1226,38 +1226,53 @@ impl From for widget::Id { } } +impl From<&'static str> for Id { + fn from(id: &'static str) -> Self { + Self::new(id) + } +} + +impl From for Id { + fn from(id: String) -> Self { + Self::new(id) + } +} + /// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus(id: Id) -> Task { - task::effect(Action::widget(operation::focusable::focus(id.0))) +pub fn focus(id: impl Into) -> Task { + task::effect(Action::widget(operation::focusable::focus(id.into().0))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. -pub fn move_cursor_to_end(id: Id) -> Task { +pub fn move_cursor_to_end(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to_end( - id.0, + id.into().0, ))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. -pub fn move_cursor_to_front(id: Id) -> Task { +pub fn move_cursor_to_front(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to_front( - id.0, + id.into().0, ))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. -pub fn move_cursor_to(id: Id, position: usize) -> Task { +pub fn move_cursor_to(id: impl Into, position: usize) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to( - id.0, position, + id.into().0, + position, ))) } /// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all(id: Id) -> Task { - task::effect(Action::widget(operation::text_input::select_all(id.0))) +pub fn select_all(id: impl Into) -> Task { + task::effect(Action::widget(operation::text_input::select_all( + id.into().0, + ))) } /// The state of a [`TextInput`]. From 45992109ddea00b24484e68ac78ae4b55d2c6c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 20:41:49 +0200 Subject: [PATCH 296/657] Fix scrolling direction with trackpad in `scrollable` --- 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 af6a3945..a5610166 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -751,7 +751,7 @@ where // TODO: Configurable speed/friction (?) -movement * 60.0 } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + mouse::ScrollDelta::Pixels { x, y } => -Vector::new(x, y), }; state.scroll( From 72e75e04914a7663b74a90ece004f135f5012137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 20:45:56 +0200 Subject: [PATCH 297/657] Remove trailing `0` in `rust-version` --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 296fef98..82b60faa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,13 @@ name = "iced" description = "A cross-platform GUI library inspired by Elm" version.workspace = true edition.workspace = true -rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true categories.workspace = true keywords.workspace = true +rust-version.workspace = true [lints] workspace = true @@ -120,12 +120,12 @@ members = [ version = "0.13.0-dev" authors = ["Héctor Ramón Jiménez "] edition = "2021" -rust-version = "1.75.0" license = "MIT" repository = "https://github.com/iced-rs/iced" homepage = "https://iced.rs" categories = ["gui"] keywords = ["gui", "ui", "graphics", "interface", "widgets"] +rust-version = "1.75" [workspace.dependencies] iced = { version = "0.13.0-dev", path = "." } From cfe912cbc32772fb9549c1f17fce202f3eb2ad99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:03:37 +0200 Subject: [PATCH 298/657] Add MSRV to test matrix in GitHub CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47c61f5e..d75c59ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - rust: [stable, beta] + rust: [stable, beta, 1.75] steps: - uses: hecrj/setup-rust-action@v2 with: From 14353c285f4e65bd58e7d109a6dc7d598dd9cb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:12:50 +0200 Subject: [PATCH 299/657] Bump MSRV to `1.77` --- .github/workflows/test.yml | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d75c59ea..225b3d92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - rust: [stable, beta, 1.75] + rust: [stable, beta, 1.77] steps: - uses: hecrj/setup-rust-action@v2 with: diff --git a/Cargo.toml b/Cargo.toml index 82b60faa..35133f97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ repository = "https://github.com/iced-rs/iced" homepage = "https://iced.rs" categories = ["gui"] keywords = ["gui", "ui", "graphics", "interface", "widgets"] -rust-version = "1.75" +rust-version = "1.77" [workspace.dependencies] iced = { version = "0.13.0-dev", path = "." } From d20ce8d82cb4936602c57064a896f7ed686529be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:19:18 +0200 Subject: [PATCH 300/657] Import `Executor` directly from `crate` --- src/application.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/application.rs b/src/application.rs index d175cf85..2ba764be 100644 --- a/src/application.rs +++ b/src/application.rs @@ -30,11 +30,11 @@ //! ] //! } //! ``` -use iced_futures::Executor; - use crate::program::{self, Program}; use crate::window; -use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; +use crate::{ + Element, Executor, Font, Result, Settings, Size, Subscription, Task, +}; use std::borrow::Cow; From 93068836182cb2a6527a267453f280eb5c0d34a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:19:33 +0200 Subject: [PATCH 301/657] Fix order of `Program::theme` implementation --- src/program.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/program.rs b/src/program.rs index 68efab88..94cb9a7d 100644 --- a/src/program.rs +++ b/src/program.rs @@ -570,14 +570,6 @@ pub fn with_executor( type Renderer = P::Renderer; type Executor = E; - fn theme( - &self, - state: &Self::State, - window: window::Id, - ) -> Self::Theme { - self.program.theme(state, window) - } - fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -605,6 +597,14 @@ pub fn with_executor( self.program.subscription(state) } + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + fn style( &self, state: &Self::State, From 11ac9125491c4d743c393857445b9b67ea5a437a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:45:25 +0200 Subject: [PATCH 302/657] Fix `scrollable` transactions when `on_scroll` is not set --- widget/src/scrollable.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index af6a3945..b8de66e3 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -760,13 +760,17 @@ where content_bounds, ); - if notify_scroll( + let has_scrolled = notify_scroll( state, &self.on_scroll, bounds, content_bounds, shell, - ) { + ); + + let in_transaction = state.last_scrolled.is_some(); + + if has_scrolled || in_transaction { event::Status::Captured } else { event::Status::Ignored @@ -1194,11 +1198,6 @@ fn notify_viewport( return false; } - let Some(on_scroll) = on_scroll else { - state.last_notified = None; - return false; - }; - let viewport = Viewport { offset_x: state.offset_x, offset_y: state.offset_y, @@ -1229,9 +1228,12 @@ fn notify_viewport( } } - shell.publish(on_scroll(viewport)); state.last_notified = Some(viewport); + if let Some(on_scroll) = on_scroll { + shell.publish(on_scroll(viewport)); + } + true } From 7f4a73e1856134821e3e0af11f21469cdc4544a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:21:02 +0200 Subject: [PATCH 303/657] Implement `executor` method for `Daemon` --- src/daemon.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/daemon.rs b/src/daemon.rs index 6a6ad133..81254bf9 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -2,7 +2,7 @@ use crate::application; use crate::program::{self, Program}; use crate::window; -use crate::{Element, Font, Result, Settings, Subscription, Task}; +use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; use std::borrow::Cow; @@ -223,6 +223,21 @@ impl Daemon

{ settings: self.settings, } } + + /// Sets the executor of the [`Daemon`]. + pub fn executor( + self, + ) -> Daemon< + impl Program, + > + where + E: Executor, + { + Daemon { + raw: program::with_executor::(self.raw), + settings: self.settings, + } + } } /// The title logic of some [`Daemon`]. From 71af846c6d0feab483a2b4e8bcd49a2ccc31f478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:51:12 +0200 Subject: [PATCH 304/657] Remove redundant import in `markdown` widget --- 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 fa4ee6bf..4bcd3353 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -239,7 +239,7 @@ pub fn parse(markdown: &str) -> impl Iterator + '_ { ) if !metadata && !table => { #[cfg(feature = "highlighter")] { - use iced_highlighter::{self, Highlighter}; + use iced_highlighter::Highlighter; use text::Highlighter as _; highlighter = From 15e6c949d73fa43285ad713c5abe32355823956f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 21:51:19 +0200 Subject: [PATCH 305/657] Bump MSRV to `1.80` --- .github/workflows/test.yml | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 225b3d92..ea941509 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - rust: [stable, beta, 1.77] + rust: [stable, beta, "1.80"] steps: - uses: hecrj/setup-rust-action@v2 with: diff --git a/Cargo.toml b/Cargo.toml index 35133f97..04ac35ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ repository = "https://github.com/iced-rs/iced" homepage = "https://iced.rs" categories = ["gui"] keywords = ["gui", "ui", "graphics", "interface", "widgets"] -rust-version = "1.77" +rust-version = "1.80" [workspace.dependencies] iced = { version = "0.13.0-dev", path = "." } From 9e5afc54cea74c6d06a9d3f1ae0f7372ff1e13cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 22:00:40 +0200 Subject: [PATCH 306/657] Show `Action` pattern in The Pocket Guide --- src/lib.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 022f8d6e..0ff9e3aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -380,16 +380,18 @@ //! # use iced::{Element, Task}; //! # pub struct Contacts; //! # impl Contacts { -//! # pub fn update(&mut self, message: Message) -> Task { unimplemented!() } +//! # pub fn update(&mut self, message: Message) -> Action { unimplemented!() } //! # pub fn view(&self) -> Element { unimplemented!() } //! # } //! # #[derive(Debug)] //! # pub enum Message {} +//! # pub enum Action { None, Run(Task), Chat(()) } //! # } //! # mod conversation { //! # use iced::{Element, Task}; //! # pub struct Conversation; //! # impl Conversation { +//! # pub fn new(contact: ()) -> (Self, Task) { unimplemented!() } //! # pub fn update(&mut self, message: Message) -> Task { unimplemented!() } //! # pub fn view(&self) -> Element { unimplemented!() } //! # } @@ -419,7 +421,19 @@ //! match message { //! Message::Contacts(message) => { //! if let Screen::Contacts(contacts) = &mut state.screen { -//! contacts.update(message).map(Message::Contacts) +//! let action = contacts.update(message); +//! +//! match action { +//! contacts::Action::None => Task::none(), +//! contacts::Action::Run(task) => task.map(Message::Contacts), +//! contacts::Action::Chat(contact) => { +//! let (conversation, task) = Conversation::new(contact); +//! +//! state.screen = Screen::Conversation(conversation); +//! +//! task.map(Message::Conversation) +//! } +//! } //! } else { //! Task::none() //! } From 1ada297b082ffa79f7e56c14af20b359af745a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 22:08:43 +0200 Subject: [PATCH 307/657] Explain `Action` pattern a bit in The Pocket Guide --- src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0ff9e3aa..91d8e53f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -456,8 +456,16 @@ //! } //! ``` //! -//! Functor methods like [`Task::map`], [`Element::map`], and [`Subscription::map`] make this -//! approach seamless. +//! The `update` method of a screen can return an `Action` enum that can be leveraged by the parent to +//! execute a task or transition to a completely different screen altogether. The variants of `Action` can +//! have associated data. For instance, in the example above, the `Conversation` screen is created when +//! `Contacts::update` returns an `Action::Chat` with the selected contact. +//! +//! Effectively, this approach lets you "tell a story" to connect different screens together in a type safe +//! way. +//! +//! Furthermore, functor methods like [`Task::map`], [`Element::map`], and [`Subscription::map`] make composition +//! seamless. #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/bdf0430880f5c29443f5f0a0ae4895866dfef4c6/docs/logo.svg" )] From ad74e4c69d9c7d65fcb3f081e76e358b60e0e51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 22:13:09 +0200 Subject: [PATCH 308/657] Improve imports of `Subscription::run` doc example --- futures/src/subscription.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 946995d8..8067c259 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -138,11 +138,16 @@ impl Subscription { /// and returning the `Sender` as a `Message` for the `Application`: /// /// ``` - /// use iced_futures::subscription::{self, Subscription}; - /// use iced_futures::stream; - /// use iced_futures::futures::channel::mpsc; - /// use iced_futures::futures::sink::SinkExt; - /// use iced_futures::futures::Stream; + /// # mod iced { + /// # pub use iced_futures::Subscription; + /// # pub use iced_futures::futures; + /// # pub use iced_futures::stream; + /// # } + /// use iced::futures::channel::mpsc; + /// use iced::futures::sink::SinkExt; + /// use iced::futures::Stream; + /// use iced::stream; + /// use iced::Subscription; /// /// pub enum Event { /// Ready(mpsc::Sender), From ab2cedc7d7de6cb4892a73e48b20bb9bab6d661b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 22:19:29 +0200 Subject: [PATCH 309/657] Remove outdated references in `README` and `ECOSYSTEM` guide --- ECOSYSTEM.md | 91 ---------------------------------------------------- README.md | 23 +++---------- 2 files changed, 5 insertions(+), 109 deletions(-) delete mode 100644 ECOSYSTEM.md diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md deleted file mode 100644 index da3066d8..00000000 --- a/ECOSYSTEM.md +++ /dev/null @@ -1,91 +0,0 @@ -# Ecosystem -This document describes the Iced ecosystem and explains how the different crates relate to each other. - -## Overview -Iced is meant to be used by 2 different types of users: - -- __End-users__. They should be able to: - - get started quickly, - - have many widgets available, - - keep things simple, - - and build applications that are __maintainable__ and __performant__. -- __GUI toolkit developers / Ecosystem contributors__. They should be able to: - - build new kinds of widgets, - - implement custom runtimes, - - integrate existing runtimes in their own system (like game engines), - - and create their own custom renderers. - -Iced consists of different crates which offer different layers of abstractions for our users. This modular architecture helps us keep implementation details hidden and decoupled, which should allow us to rewrite or change strategies in the future. - -

- The Iced Ecosystem -

- -## The foundations -There are a bunch of concepts that permeate the whole ecosystem. These concepts are considered __the foundations__, and they are provided by three different crates: - -- [`iced_core`] contains many lightweight, reusable primitives (e.g. `Point`, `Rectangle`, `Color`). -- [`iced_futures`] implements the concurrent concepts of [The Elm Architecture] on top of the [`futures`] ecosystem. -- [`iced_style`] defines the default styling capabilities of built-in widgets. - -

- The foundations -

- -## The native target -The native side of the ecosystem is split into two different groups: __renderers__ and __shells__. - -

- The native target -

- -### Renderers -The widgets of a _graphical_ user interface produce some primitives that eventually need to be drawn on screen. __Renderers__ take care of this task, potentially leveraging GPU acceleration. - -Currently, there are two different official renderers: - -- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal. -- [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported. - -Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate. - -### Shells -The widgets of a graphical user _interface_ are interactive. __Shells__ gather and process user interactions in an event loop. - -Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture]. - -As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`]. - -## The web target -The Web platform provides all the abstractions necessary to draw widgets and gather users interactions. - -Therefore, unlike the native path, the web side of the ecosystem does not need to split renderers and shells. Instead, [`iced_web`] leverages [`dodrio`] to both render widgets and implement a proper runtime. - -## Iced -Finally, [`iced`] unifies everything into a simple abstraction to create cross-platform applications: - -- On native, it uses __[shells](#shells)__ and __[renderers](#renderers)__. -- On the web, it uses [`iced_web`]. - -

- Iced -

- -[`iced_core`]: core -[`iced_futures`]: futures -[`iced_style`]: style -[`iced_native`]: native -[`iced_web`]: https://github.com/iced-rs/iced_web -[`iced_graphics`]: graphics -[`iced_wgpu`]: wgpu -[`iced_glow`]: glow -[`iced_winit`]: winit -[`iced_glutin`]: glutin -[`iced`]: .. -[`futures`]: https://github.com/rust-lang/futures-rs -[`glow`]: https://github.com/grovesNL/glow -[`wgpu`]: https://github.com/gfx-rs/wgpu -[`winit`]: https://github.com/rust-windowing/winit -[`glutin`]: https://github.com/rust-windowing/glutin -[`dodrio`]: https://github.com/fitzgen/dodrio -[The Elm Architecture]: https://guide.elm-lang.org/architecture/ diff --git a/README.md b/README.md index a9c37977..41fa220a 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,12 @@ Inspired by [Elm]. * Custom widget support (create your own!) * [Debug overlay with performance metrics] * First-class support for async actions (use futures!) -* [Modular ecosystem] split into reusable parts: +* Modular ecosystem split into reusable parts: * A [renderer-agnostic native runtime] enabling integration with existing systems - * Two [built-in renderers] leveraging [`wgpu`] and [`tiny-skia`] + * Two built-in renderers leveraging [`wgpu`] and [`tiny-skia`] * [`iced_wgpu`] supporting Vulkan, Metal and DX12 * [`iced_tiny_skia`] offering a software alternative as a fallback * A [windowing shell] - * A [web runtime] leveraging the DOM __Iced is currently experimental software.__ [Take a look at the roadmap], [check out the issues], and [feel free to contribute!] @@ -49,16 +48,12 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], [text inputs]: https://iced.rs/examples/text_input.mp4 [scrollables]: https://iced.rs/examples/scrollable.mp4 [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4 -[Modular ecosystem]: ECOSYSTEM.md [renderer-agnostic native runtime]: runtime/ [`wgpu`]: https://github.com/gfx-rs/wgpu [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia [`iced_wgpu`]: wgpu/ [`iced_tiny_skia`]: tiny_skia/ -[built-in renderers]: ECOSYSTEM.md#Renderers [windowing shell]: winit/ -[`dodrio`]: https://github.com/fitzgen/dodrio -[web runtime]: https://github.com/iced-rs/iced_web [Take a look at the roadmap]: ROADMAP.md [check out the issues]: https://github.com/iced-rs/iced/issues [feel free to contribute!]: #contributing--feedback @@ -164,7 +159,7 @@ Read the [book], the [documentation], and the [examples] to learn more! ## Implementation details Iced was originally born as an attempt at bringing the simplicity of [Elm] and -[The Elm Architecture] into [Coffee], a 2D game engine I am working on. +[The Elm Architecture] into [Coffee], a 2D game library I am working on. The core of the library was implemented during May 2019 in [this pull request]. [The first alpha version] was eventually released as @@ -172,25 +167,17 @@ The core of the library was implemented during May 2019 in [this pull request]. implemented the current [tour example] on top of [`ggez`], a game library. Since then, the focus has shifted towards providing a batteries-included, -end-user-oriented GUI library, while keeping [the ecosystem] modular: - -

- - The Iced Ecosystem - -

+end-user-oriented GUI library, while keeping the ecosystem modular. [this pull request]: https://github.com/hecrj/coffee/pull/35 [The first alpha version]: https://github.com/iced-rs/iced/tree/0.1.0-alpha [a renderer-agnostic GUI library]: https://www.reddit.com/r/rust/comments/czzjnv/iced_a_rendereragnostic_gui_library_focused_on/ [tour example]: examples/README.md#tour [`ggez`]: https://github.com/ggez/ggez -[the ecosystem]: ECOSYSTEM.md ## Contributing / Feedback -Contributions are greatly appreciated! If you want to contribute, please -read our [contributing guidelines] for more details. +If you want to contribute, please read our [contributing guidelines] for more details. Feedback is also welcome! You can create a new topic in [our Discourse forum] or come chat to [our Discord server]. From ed4d0781c7172e9ed1f047a37f01a908c7896bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 22:20:35 +0200 Subject: [PATCH 310/657] Remove "feel free to contribute!" link in `README` --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 41fa220a..17804939 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ Inspired by [Elm]. * [`iced_tiny_skia`] offering a software alternative as a fallback * A [windowing shell] -__Iced is currently experimental software.__ [Take a look at the roadmap], -[check out the issues], and [feel free to contribute!] +__Iced is currently experimental software.__ [Take a look at the roadmap] and +[check out the issues]. [Cross-platform support]: https://raw.githubusercontent.com/iced-rs/iced/master/docs/images/todos_desktop.jpg [text inputs]: https://iced.rs/examples/text_input.mp4 @@ -56,7 +56,6 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], [windowing shell]: winit/ [Take a look at the roadmap]: ROADMAP.md [check out the issues]: https://github.com/iced-rs/iced/issues -[feel free to contribute!]: #contributing--feedback ## Overview From dc2efb3fabc5378671eb91f0983464f372143c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 22:25:59 +0200 Subject: [PATCH 311/657] Showcase `halloy` and `icebreaker` in `README` :tada: --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 17804939..9cfa03de 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by [Elm]. - - + + - - + + From 0a95af78f46728b86cb78380791f40f008be99eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 18 Sep 2024 23:05:50 +0200 Subject: [PATCH 312/657] Add quick example to `widget::button` module --- widget/src/button.rs | 46 +++++++++++++++++++++++++++++++------------ widget/src/helpers.rs | 16 ++++++++++++++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/widget/src/button.rs b/widget/src/button.rs index eafa71b9..a76035f7 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,4 +1,20 @@ -//! Allow your users to perform actions by pressing a button. +//! Buttons allow your users to perform actions by pressing them. +//! +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::button; +//! +//! #[derive(Clone)] +//! enum Message { +//! ButtonPressed, +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! button("Press me!").on_press(Message::ButtonPressed).into() +//! } +//! ``` use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout; @@ -17,33 +33,37 @@ use crate::core::{ /// A generic widget that produces a message when pressed. /// /// ```no_run -/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; -/// # +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::button; +/// /// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } /// -/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); +/// fn view(state: &State) -> Element<'_, Message> { +/// button("Press me!").on_press(Message::ButtonPressed).into() +/// } /// ``` /// /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will /// be disabled: /// -/// ``` -/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; -/// # +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::button; +/// /// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } /// -/// fn disabled_button<'a>() -> Button<'a, Message> { -/// Button::new("I'm disabled!") -/// } -/// -/// fn enabled_button<'a>() -> Button<'a, Message> { -/// disabled_button().on_press(Message::ButtonPressed) +/// fn view(state: &State) -> Element<'_, Message> { +/// button("I am disabled!").into() /// } /// ``` #[allow(missing_debug_implementations)] diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 51978823..2ad62156 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -653,7 +653,21 @@ where /// Creates a new [`Button`] with the provided content. /// -/// [`Button`]: crate::Button +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::button; +/// +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// button("Press me!").on_press(Message::ButtonPressed).into() +/// } +/// ``` pub fn button<'a, Message, Theme, Renderer>( content: impl Into>, ) -> Button<'a, Message, Theme, Renderer> From 5d25562644907488203604769f338feb2c7df9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 02:47:03 +0200 Subject: [PATCH 313/657] Show `canvas` doc example in multiple places --- widget/src/canvas.rs | 86 +++++++++++++++++++++++++++++++++++++------ widget/src/helpers.rs | 51 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 185fa082..fb6d55e1 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -1,4 +1,55 @@ //! Draw 2D graphics for your users. +//! +//! ## Drawing a simple circle +//! Here's how we can use a [`Canvas`] to draw a simple circle: +//! +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::mouse; +//! use iced::widget::canvas; +//! use iced::{Color, Rectangle, Renderer, Theme}; +//! +//! // First, we define the data we need for drawing +//! #[derive(Debug)] +//! struct Circle { +//! radius: f32, +//! } +//! +//! // Then, we implement the `Program` trait +//! impl canvas::Program for Circle { +//! // No internal state +//! type State = (); +//! +//! fn draw( +//! &self, +//! _state: &(), +//! renderer: &Renderer, +//! _theme: &Theme, +//! bounds: Rectangle, +//! _cursor: mouse::Cursor +//! ) -> Vec { +//! // We prepare a new `Frame` +//! let mut frame = canvas::Frame::new(renderer, bounds.size()); +//! +//! // We create a `Path` representing a simple circle +//! let circle = canvas::Path::circle(frame.center(), self.radius); +//! +//! // And fill it with some color +//! frame.fill(&circle, Color::BLACK); +//! +//! // Then, we produce the geometry +//! vec![frame.into_geometry()] +//! } +//! } +//! +//! // Finally, we simply use our `Circle` to create the `Canvas`! +//! fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> { +//! canvas(Circle { radius: 50.0 }).into() +//! } +//! ``` pub mod event; mod program; @@ -40,14 +91,17 @@ pub type Frame = geometry::Frame; /// A widget capable of drawing 2D graphics. /// /// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: +/// Here's how we can use a [`Canvas`] to draw a simple circle: /// /// ```no_run -/// # use iced_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program}; -/// # use iced_widget::core::{Color, Rectangle}; -/// # use iced_widget::core::mouse; -/// # use iced_widget::{Renderer, Theme}; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # +/// use iced::mouse; +/// use iced::widget::canvas; +/// use iced::{Color, Rectangle, Renderer, Theme}; +/// /// // First, we define the data we need for drawing /// #[derive(Debug)] /// struct Circle { @@ -55,26 +109,36 @@ pub type Frame = geometry::Frame; /// } /// /// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { +/// impl canvas::Program for Circle { +/// // No internal state /// type State = (); /// -/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec { +/// fn draw( +/// &self, +/// _state: &(), +/// renderer: &Renderer, +/// _theme: &Theme, +/// bounds: Rectangle, +/// _cursor: mouse::Cursor +/// ) -> Vec { /// // We prepare a new `Frame` -/// let mut frame = Frame::new(renderer, bounds.size()); +/// let mut frame = canvas::Frame::new(renderer, bounds.size()); /// /// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); +/// let circle = canvas::Path::circle(frame.center(), self.radius); /// /// // And fill it with some color /// frame.fill(&circle, Color::BLACK); /// -/// // Finally, we produce the geometry +/// // Then, we produce the geometry /// vec![frame.into_geometry()] /// } /// } /// /// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> { +/// canvas(Circle { radius: 50.0 }).into() +/// } /// ``` #[derive(Debug)] pub struct Canvas diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 2ad62156..30d40edc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1000,6 +1000,57 @@ where /// Creates a new [`Canvas`]. /// /// [`Canvas`]: crate::Canvas +/// +/// ## Drawing a simple circle +/// Here's how we can use a [`Canvas`] to draw a simple circle: +/// +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::mouse; +/// use iced::widget::canvas; +/// use iced::{Color, Rectangle, Renderer, Theme}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl canvas::Program for Circle { +/// // No internal state +/// type State = (); +/// +/// fn draw( +/// &self, +/// _state: &(), +/// renderer: &Renderer, +/// _theme: &Theme, +/// bounds: Rectangle, +/// _cursor: mouse::Cursor +/// ) -> Vec { +/// // We prepare a new `Frame` +/// let mut frame = canvas::Frame::new(renderer, bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = canvas::Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Then, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> { +/// canvas(Circle { radius: 50.0 }).into() +/// } +/// ``` #[cfg(feature = "canvas")] pub fn canvas( program: P, From 51f7ce73248bea2bb7cc2b662433e46fa0c44dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 03:03:11 +0200 Subject: [PATCH 314/657] Show `checkbox` doc example in multiple places --- widget/src/button.rs | 2 ++ widget/src/canvas.rs | 4 +-- widget/src/checkbox.rs | 60 +++++++++++++++++++++++++++++++++++++----- widget/src/helpers.rs | 34 ++++++++++++++++++++++-- 4 files changed, 89 insertions(+), 11 deletions(-) diff --git a/widget/src/button.rs b/widget/src/button.rs index a76035f7..3323c0d3 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,5 +1,6 @@ //! Buttons allow your users to perform actions by pressing them. //! +//! # Example //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } } //! # pub type State = (); @@ -32,6 +33,7 @@ use crate::core::{ /// A generic widget that produces a message when pressed. /// +/// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index fb6d55e1..bb6b0353 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -1,6 +1,6 @@ //! Draw 2D graphics for your users. //! -//! ## Drawing a simple circle +//! # Example: Drawing a Simple Circle //! Here's how we can use a [`Canvas`] to draw a simple circle: //! //! ```no_run @@ -90,7 +90,7 @@ pub type Frame = geometry::Frame; /// A widget capable of drawing 2D graphics. /// -/// ## Drawing a simple circle +/// # Example: Drawing a Simple Circle /// Here's how we can use a [`Canvas`] to draw a simple circle: /// /// ```no_run diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 32db5090..4a3f35ed 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,4 +1,35 @@ -//! Show toggle controls using checkboxes. +//! Checkboxes can be used to let users make binary choices. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::checkbox; +//! +//! struct State { +//! is_checked: bool, +//! } +//! +//! enum Message { +//! CheckboxToggled(bool), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! checkbox("Toggle me!", state.is_checked) +//! .on_toggle(Message::CheckboxToggled) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::CheckboxToggled(is_checked) => { +//! state.is_checked = is_checked; +//! } +//! } +//! } +//! ``` +//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout; @@ -17,19 +48,34 @@ use crate::core::{ /// A box that can be checked. /// /// # Example -/// /// ```no_run -/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// pub enum Message { +/// use iced::widget::checkbox; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { /// CheckboxToggled(bool), /// } /// -/// let is_checked = true; +/// fn view(state: &State) -> Element<'_, Message> { +/// checkbox("Toggle me!", state.is_checked) +/// .on_toggle(Message::CheckboxToggled) +/// .into() +/// } /// -/// Checkbox::new("Toggle me!", is_checked).on_toggle(Message::CheckboxToggled); +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::CheckboxToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } /// ``` -/// /// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) #[allow(missing_debug_implementations)] pub struct Checkbox< diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 30d40edc..1ed0bde2 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -653,6 +653,7 @@ where /// Creates a new [`Button`] with the provided content. /// +/// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); @@ -747,7 +748,36 @@ pub use crate::markdown::view as markdown; /// Creates a new [`Checkbox`]. /// -/// [`Checkbox`]: crate::Checkbox +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::checkbox; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { +/// CheckboxToggled(bool), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// checkbox("Toggle me!", state.is_checked) +/// .on_toggle(Message::CheckboxToggled) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::CheckboxToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } +/// ``` +/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) pub fn checkbox<'a, Message, Theme, Renderer>( label: impl Into, is_checked: bool, @@ -1001,7 +1031,7 @@ where /// /// [`Canvas`]: crate::Canvas /// -/// ## Drawing a simple circle +/// # Example: Drawing a Simple Circle /// Here's how we can use a [`Canvas`] to draw a simple circle: /// /// ```no_run From 3e6e669c4c111b2ac65c7aff1d03c1a1f5ba645f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 03:18:08 +0200 Subject: [PATCH 315/657] Show `combo_box` doc example in multiple places --- widget/src/combo_box.rs | 114 ++++++++++++++++++++++++++++++++++++++-- widget/src/helpers.rs | 57 +++++++++++++++++++- 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index a51701ca..fb661ad5 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -1,4 +1,59 @@ -//! Display a dropdown list of searchable and selectable options. +//! Combo boxes display a dropdown list of searchable and selectable options. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::combo_box; +//! +//! struct State { +//! fruits: combo_box::State, +//! favorite: Option, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Fruit { +//! Apple, +//! Orange, +//! Strawberry, +//! Tomato, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! FruitSelected(Fruit), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! combo_box( +//! &state.fruits, +//! "Select your favorite fruit...", +//! state.favorite.as_ref(), +//! Message::FruitSelected +//! ) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::FruitSelected(fruit) => { +//! state.favorite = Some(fruit); +//! } +//! } +//! } +//! +//! impl std::fmt::Display for Fruit { +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +//! f.write_str(match self { +//! Self::Apple => "Apple", +//! Self::Orange => "Orange", +//! Self::Strawberry => "Strawberry", +//! Self::Tomato => "Tomato", +//! }) +//! } +//! } +//! ``` use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key; @@ -21,9 +76,60 @@ use std::fmt::Display; /// A widget for searching and selecting a single value from a list of options. /// -/// This widget is composed by a [`TextInput`] that can be filled with the text -/// to search for corresponding values from the list of options that are displayed -/// as a Menu. +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::combo_box; +/// +/// struct State { +/// fruits: combo_box::State, +/// favorite: Option, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// combo_box( +/// &state.fruits, +/// "Select your favorite fruit...", +/// state.favorite.as_ref(), +/// Message::FruitSelected +/// ) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct ComboBox< 'a, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1ed0bde2..d839bc3a 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -902,7 +902,62 @@ where /// Creates a new [`ComboBox`]. /// -/// [`ComboBox`]: crate::ComboBox +/// Combo boxes display a dropdown list of searchable and selectable options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::combo_box; +/// +/// struct State { +/// fruits: combo_box::State, +/// favorite: Option, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// combo_box( +/// &state.fruits, +/// "Select your favorite fruit...", +/// state.favorite.as_ref(), +/// Message::FruitSelected +/// ) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` pub fn combo_box<'a, T, Message, Theme, Renderer>( state: &'a combo_box::State, placeholder: &str, From 96615d5537d04f0b8d3938f417e08bbc3c34e1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 03:33:09 +0200 Subject: [PATCH 316/657] Show `container` doc example in multiple places --- widget/src/canvas.rs | 6 +----- widget/src/container.rs | 44 ++++++++++++++++++++++++++++++++++++++--- widget/src/helpers.rs | 26 +++++++++++++++++++++--- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index bb6b0353..9fbccf82 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -1,8 +1,6 @@ -//! Draw 2D graphics for your users. +//! Canvases can be leveraged to draw interactive 2D graphics. //! //! # Example: Drawing a Simple Circle -//! Here's how we can use a [`Canvas`] to draw a simple circle: -//! //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } //! # pub type State = (); @@ -91,8 +89,6 @@ pub type Frame = geometry::Frame; /// A widget capable of drawing 2D graphics. /// /// # Example: Drawing a Simple Circle -/// Here's how we can use a [`Canvas`] to draw a simple circle: -/// /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } /// # pub type State = (); diff --git a/widget/src/container.rs b/widget/src/container.rs index 3b794099..b256540c 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -1,4 +1,24 @@ -//! Decorate content and apply alignment. +//! Containers let you align a widget inside their boundaries. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::container; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! container("This text is centered inside a rounded box!") +//! .padding(10) +//! .center(800) +//! .style(container::rounded_box) +//! .into() +//! } +//! ``` use crate::core::alignment::{self, Alignment}; use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; @@ -16,9 +36,27 @@ use crate::core::{ }; use crate::runtime::task::{self, Task}; -/// An element decorating some content. +/// A widget that aligns its contents inside of its boundaries. /// -/// It is normally used for alignment purposes. +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::container; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// container("This text is centered inside a rounded box!") +/// .padding(10) +/// .center(800) +/// .style(container::rounded_box) +/// .into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Container< 'a, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index d839bc3a..0699446d 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -128,7 +128,27 @@ macro_rules! rich_text { /// Creates a new [`Container`] with the provided content. /// -/// [`Container`]: crate::Container +/// Containers let you align a widget inside their boundaries. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::container; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// container("This text is centered inside a rounded box!") +/// .padding(10) +/// .center(800) +/// .style(container::rounded_box) +/// .into() +/// } +/// ``` pub fn container<'a, Message, Theme, Renderer>( content: impl Into>, ) -> Container<'a, Message, Theme, Renderer> @@ -1084,11 +1104,11 @@ where /// Creates a new [`Canvas`]. /// +/// Canvases can be leveraged to draw interactive 2D graphics. +/// /// [`Canvas`]: crate::Canvas /// /// # Example: Drawing a Simple Circle -/// Here's how we can use a [`Canvas`] to draw a simple circle: -/// /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } /// # pub type State = (); From e98a441b0fa0c1e2ddffe615a48f3fe23a4fedad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 03:40:38 +0200 Subject: [PATCH 317/657] Show `image` doc example in multiple places --- widget/src/helpers.rs | 19 +++++++++++++++++++ widget/src/image.rs | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 0699446d..bae2e10e 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1047,7 +1047,26 @@ where /// Creates a new [`Image`]. /// +/// Images display raster graphics in different formats (PNG, JPG, etc.). +/// /// [`Image`]: crate::Image +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::image; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// image("ferris.png").into() +/// } +/// ``` +/// #[cfg(feature = "image")] pub fn image(handle: impl Into) -> crate::Image { crate::Image::new(handle.into()) diff --git a/widget/src/image.rs b/widget/src/image.rs index e04f2d6f..c8f2a620 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,4 +1,21 @@ -//! Display images in your user interface. +//! Images display raster graphics in different formats (PNG, JPG, etc.). +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::image; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! image("ferris.png").into() +//! } +//! ``` +//! pub mod viewer; pub use viewer::Viewer; @@ -22,16 +39,23 @@ pub fn viewer(handle: Handle) -> Viewer { /// A frame that displays an image while keeping aspect ratio. /// /// # Example -/// /// ```no_run -/// # use iced_widget::image::{self, Image}; -/// # -/// let image = Image::::new("resources/ferris.png"); -/// ``` +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::image; /// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// image("ferris.png").into() +/// } +/// ``` /// #[derive(Debug)] -pub struct Image { +pub struct Image { handle: Handle, width: Length, height: Length, From 70dd0501af2084f410b4511d6ddc63296b643f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 03:54:29 +0200 Subject: [PATCH 318/657] Show `keyed_column` doc example in multiple places --- widget/src/helpers.rs | 22 +++++++++++++++++++++- widget/src/keyed.rs | 32 +++++++++++++++++++++++++++----- widget/src/keyed/column.rs | 22 ++++++++++++++++++++-- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index bae2e10e..11eed6ba 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -191,7 +191,27 @@ where Column::with_children(children) } -/// Creates a new [`keyed::Column`] with the given children. +/// Creates a new [`keyed::Column`] from an iterator of elements. +/// +/// Keyed columns distribute content vertically while keeping continuity. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{keyed_column, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// keyed_column((0..=100).map(|i| { +/// (i, text!("Item {i}").into()) +/// })).into() +/// } +/// ``` pub fn keyed_column<'a, Key, Message, Theme, Renderer>( children: impl IntoIterator)>, ) -> keyed::Column<'a, Key, Message, Theme, Renderer> diff --git a/widget/src/keyed.rs b/widget/src/keyed.rs index ad531e66..923cb118 100644 --- a/widget/src/keyed.rs +++ b/widget/src/keyed.rs @@ -1,4 +1,4 @@ -//! Use widgets that can provide hints to ensure continuity. +//! Keyed widgets can provide hints to ensure continuity. //! //! # What is continuity? //! Continuity is the feeling of persistence of state. @@ -41,13 +41,35 @@ pub mod column; pub use column::Column; -/// Creates a [`Column`] with the given children. +/// Creates a keyed [`Column`] with the given children. +/// +/// Keyed columns distribute content vertically while keeping continuity. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::keyed_column; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// keyed_column![ +/// (0, "Item 0"), +/// (1, "Item 1"), +/// (2, "Item 2"), +/// ].into() +/// } +/// ``` #[macro_export] macro_rules! keyed_column { () => ( - $crate::Column::new() + $crate::keyed::Column::new() ); - ($($x:expr),+ $(,)?) => ( - $crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + ($(($key:expr, $x:expr)),+ $(,)?) => ( + $crate::keyed::Column::with_children(vec![$(($key, $crate::core::Element::from($x))),+]) ); } diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 2c56c605..5852ede1 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -1,4 +1,4 @@ -//! Distribute content vertically. +//! Keyed columns distribute content vertically while keeping continuity. use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -11,7 +11,25 @@ use crate::core::{ Shell, Size, Vector, Widget, }; -/// A container that distributes its contents vertically. +/// A container that distributes its contents vertically while keeping continuity. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{keyed_column, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// keyed_column((0..=100).map(|i| { +/// (i, text!("Item {i}").into()) +/// })).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Column< 'a, From a2c16aa68e46ca3ff4e022aa05f70ef0efc1df7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 04:05:46 +0200 Subject: [PATCH 319/657] Show `markdown` doc example in multiple places --- widget/src/markdown.rs | 131 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index fa4ee6bf..6e592998 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -1,9 +1,52 @@ -//! Parse and display Markdown. +//! Markdown widgets can parse and display Markdown. //! //! You can enable the `highlighter` feature for syntax highligting //! in code blocks. //! //! Only the variants of [`Item`] are currently supported. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::markdown; +//! use iced::Theme; +//! +//! struct State { +//! markdown: Vec, +//! } +//! +//! enum Message { +//! LinkClicked(markdown::Url), +//! } +//! +//! impl State { +//! pub fn new() -> Self { +//! Self { +//! markdown: markdown::parse("This is some **Markdown**!").collect(), +//! } +//! } +//! +//! fn view(&self) -> Element<'_, Message> { +//! markdown::view( +//! &self.markdown, +//! markdown::Settings::default(), +//! markdown::Style::from_palette(Theme::TokyoNightStorm.palette()), +//! ) +//! .map(Message::LinkClicked) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::LinkClicked(url) => { +//! println!("The following url was clicked: {url}"); +//! } +//! } +//! } +//! } +//! ``` use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; @@ -145,6 +188,49 @@ impl Span { } /// Parse the given Markdown content. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::markdown; +/// use iced::Theme; +/// +/// struct State { +/// markdown: Vec, +/// } +/// +/// enum Message { +/// LinkClicked(markdown::Url), +/// } +/// +/// impl State { +/// pub fn new() -> Self { +/// Self { +/// markdown: markdown::parse("This is some **Markdown**!").collect(), +/// } +/// } +/// +/// fn view(&self) -> Element<'_, Message> { +/// markdown::view( +/// &self.markdown, +/// markdown::Settings::default(), +/// markdown::Style::from_palette(Theme::TokyoNightStorm.palette()), +/// ) +/// .map(Message::LinkClicked) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::LinkClicked(url) => { +/// println!("The following url was clicked: {url}"); +/// } +/// } +/// } +/// } +/// ``` pub fn parse(markdown: &str) -> impl Iterator + '_ { struct List { start: Option, @@ -484,6 +570,49 @@ impl Style { /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::markdown; +/// use iced::Theme; +/// +/// struct State { +/// markdown: Vec, +/// } +/// +/// enum Message { +/// LinkClicked(markdown::Url), +/// } +/// +/// impl State { +/// pub fn new() -> Self { +/// Self { +/// markdown: markdown::parse("This is some **Markdown**!").collect(), +/// } +/// } +/// +/// fn view(&self) -> Element<'_, Message> { +/// markdown::view( +/// &self.markdown, +/// markdown::Settings::default(), +/// markdown::Style::from_palette(Theme::TokyoNightStorm.palette()), +/// ) +/// .map(Message::LinkClicked) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::LinkClicked(url) => { +/// println!("The following url was clicked: {url}"); +/// } +/// } +/// } +/// } +/// ``` pub fn view<'a, Theme, Renderer>( items: impl IntoIterator, settings: Settings, From 8c6caefd9f88ae87d619b3b6f778bc91e06bad8e Mon Sep 17 00:00:00 2001 From: lufte Date: Wed, 18 Sep 2024 23:32:50 -0300 Subject: [PATCH 320/657] Set the text color determined by the style function Fixes: https://github.com/iced-rs/iced/issues/2557 --- widget/src/text_editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 1df97962..c8e7fa9a 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -742,7 +742,7 @@ where tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, - defaults: &renderer::Style, + _defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -811,7 +811,7 @@ where renderer.fill_editor( &internal.editor, text_bounds.position(), - defaults.text_color, + style.value, text_bounds, ); } From b78243d86f4e35aac7b2e126877909411404e97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 04:35:39 +0200 Subject: [PATCH 321/657] Show `pane_grid` doc example in multiple places --- widget/src/helpers.rs | 53 ++++++++++++++++++++++++++++ widget/src/pane_grid.rs | 78 +++++++++++++++++++++++++++++++++-------- 2 files changed, 117 insertions(+), 14 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 11eed6ba..ea632ecc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -9,6 +9,7 @@ use crate::core::window; use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; +use crate::pane_grid::{self, PaneGrid}; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; @@ -1269,3 +1270,55 @@ where { Themer::new(move |_| new_theme.clone(), content) } + +/// Creates a [`PaneGrid`] with the given [`pane_grid::State`] and view function. +/// +/// Pane grids let your users split regions of your application and organize layout dynamically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::{pane_grid, text}; +/// +/// struct State { +/// panes: pane_grid::State, +/// } +/// +/// enum Pane { +/// SomePane, +/// AnotherKindOfPane, +/// } +/// +/// enum Message { +/// PaneDragged(pane_grid::DragEvent), +/// PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pane_grid(&state.panes, |pane, state, is_maximized| { +/// pane_grid::Content::new(match state { +/// Pane::SomePane => text("This is some pane"), +/// Pane::AnotherKindOfPane => text("This is another kind of pane"), +/// }) +/// }) +/// .on_drag(Message::PaneDragged) +/// .on_resize(10, Message::PaneResized) +/// .into() +/// } +/// ``` +pub fn pane_grid<'a, T, Message, Theme, Renderer>( + state: &'a pane_grid::State, + view: impl Fn( + pane_grid::Pane, + &'a T, + bool, + ) -> pane_grid::Content<'a, Message, Theme, Renderer>, +) -> PaneGrid<'a, Message, Theme, Renderer> +where + Theme: pane_grid::Catalog, + Renderer: core::Renderer, +{ + PaneGrid::new(state, view) +} diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 4473119d..9d4dda25 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -1,8 +1,54 @@ -//! Let your users split regions of your application and organize layout dynamically. +//! Pane grids let your users split regions of your application and organize layout dynamically. //! //! ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif) //! +//! This distribution of space is common in tiling window managers (like +//! [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +//! [`tmux`](https://github.com/tmux/tmux)). +//! +//! A [`PaneGrid`] supports: +//! +//! * Vertical and horizontal splits +//! * Tracking of the last active pane +//! * Mouse-based resizing +//! * Drag and drop to reorganize panes +//! * Hotkey support +//! * Configurable modifier keys +//! * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +//! //! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::{pane_grid, text}; +//! +//! struct State { +//! panes: pane_grid::State, +//! } +//! +//! enum Pane { +//! SomePane, +//! AnotherKindOfPane, +//! } +//! +//! enum Message { +//! PaneDragged(pane_grid::DragEvent), +//! PaneResized(pane_grid::ResizeEvent), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! pane_grid(&state.panes, |pane, state, is_maximized| { +//! pane_grid::Content::new(match state { +//! Pane::SomePane => text("This is some pane"), +//! Pane::AnotherKindOfPane => text("This is another kind of pane"), +//! }) +//! }) +//! .on_drag(Message::PaneDragged) +//! .on_resize(10, Message::PaneResized) +//! .into() +//! } +//! ``` //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! @@ -68,14 +114,18 @@ const THICKNESS_RATIO: f32 = 25.0; /// * Configurable modifier keys /// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) /// -/// ## Example -/// +/// # Example /// ```no_run -/// # use iced_widget::{pane_grid, text}; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// # type PaneGrid<'a, Message> = iced_widget::PaneGrid<'a, Message>; -/// # -/// enum PaneState { +/// use iced::widget::{pane_grid, text}; +/// +/// struct State { +/// panes: pane_grid::State, +/// } +/// +/// enum Pane { /// SomePane, /// AnotherKindOfPane, /// } @@ -85,17 +135,17 @@ const THICKNESS_RATIO: f32 = 25.0; /// PaneResized(pane_grid::ResizeEvent), /// } /// -/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); -/// -/// let pane_grid = -/// PaneGrid::new(&state, |pane, state, is_maximized| { +/// fn view(state: &State) -> Element<'_, Message> { +/// pane_grid(&state.panes, |pane, state, is_maximized| { /// pane_grid::Content::new(match state { -/// PaneState::SomePane => text("This is some pane"), -/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), +/// Pane::SomePane => text("This is some pane"), +/// Pane::AnotherKindOfPane => text("This is another kind of pane"), /// }) /// }) /// .on_drag(Message::PaneDragged) -/// .on_resize(10, Message::PaneResized); +/// .on_resize(10, Message::PaneResized) +/// .into() +/// } /// ``` #[allow(missing_debug_implementations)] pub struct PaneGrid< From 7b22b7e87699e22f3e16869093e8a01a91d2e93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 04:45:15 +0200 Subject: [PATCH 322/657] Show `pick_list` doc example in multiple places --- widget/src/helpers.rs | 63 +++++++++++++++++++- widget/src/pick_list.rs | 124 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 2 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index ea632ecc..72d5d7fe 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -924,7 +924,68 @@ where /// Creates a new [`PickList`]. /// -/// [`PickList`]: crate::PickList +/// Pick lists display a dropdown list of selectable options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::pick_list; +/// +/// struct State { +/// favorite: Option, +/// } +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// let fruits = [ +/// Fruit::Apple, +/// Fruit::Orange, +/// Fruit::Strawberry, +/// Fruit::Tomato, +/// ]; +/// +/// pick_list( +/// fruits, +/// state.favorite, +/// Message::FruitSelected, +/// ) +/// .placeholder("Select your favorite fruit...") +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` pub fn pick_list<'a, T, L, V, Message, Theme, Renderer>( options: L, selected: Option, diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 1fc9951e..ff54fe8a 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -1,4 +1,65 @@ -//! Display a dropdown list of selectable values. +//! Pick lists display a dropdown list of selectable options. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::pick_list; +//! +//! struct State { +//! favorite: Option, +//! } +//! +//! #[derive(Debug, Clone, Copy, PartialEq, Eq)] +//! enum Fruit { +//! Apple, +//! Orange, +//! Strawberry, +//! Tomato, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! FruitSelected(Fruit), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! let fruits = [ +//! Fruit::Apple, +//! Fruit::Orange, +//! Fruit::Strawberry, +//! Fruit::Tomato, +//! ]; +//! +//! pick_list( +//! fruits, +//! state.favorite, +//! Message::FruitSelected, +//! ) +//! .placeholder("Select your favorite fruit...") +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::FruitSelected(fruit) => { +//! state.favorite = Some(fruit); +//! } +//! } +//! } +//! +//! impl std::fmt::Display for Fruit { +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +//! f.write_str(match self { +//! Self::Apple => "Apple", +//! Self::Orange => "Orange", +//! Self::Strawberry => "Strawberry", +//! Self::Tomato => "Tomato", +//! }) +//! } +//! } +//! ``` use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -20,6 +81,67 @@ use std::borrow::Borrow; use std::f32; /// A widget for selecting a single value from a list of options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::pick_list; +/// +/// struct State { +/// favorite: Option, +/// } +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// let fruits = [ +/// Fruit::Apple, +/// Fruit::Orange, +/// Fruit::Strawberry, +/// Fruit::Tomato, +/// ]; +/// +/// pick_list( +/// fruits, +/// state.favorite, +/// Message::FruitSelected, +/// ) +/// .placeholder("Select your favorite fruit...") +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct PickList< 'a, From c646ff5f1feb1112f9e34d75e40246e4fc1c74e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 04:57:32 +0200 Subject: [PATCH 323/657] Show `progress_bar` doc example in multiple places --- widget/src/helpers.rs | 22 +++++++++++++++++++- widget/src/progress_bar.rs | 41 ++++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 72d5d7fe..53286e0a 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1112,11 +1112,31 @@ where /// Creates a new [`ProgressBar`]. /// +/// Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation. +/// /// It expects: /// * an inclusive range of possible values, and /// * the current value of the [`ProgressBar`]. /// -/// [`ProgressBar`]: crate::ProgressBar +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::progress_bar; +/// +/// struct State { +/// progress: f32, +/// } +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// progress_bar(0.0..=100.0, state.progress).into() +/// } +/// ``` pub fn progress_bar<'a, Theme>( range: RangeInclusive, value: f32, diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index a10feea6..8c665c8c 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -1,4 +1,24 @@ -//! Provide progress feedback to your users. +//! Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::progress_bar; +//! +//! struct State { +//! progress: f32, +//! } +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! progress_bar(0.0..=100.0, state.progress).into() +//! } +//! ``` use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; @@ -15,14 +35,23 @@ use std::ops::RangeInclusive; /// /// # Example /// ```no_run -/// # type ProgressBar<'a> = iced_widget::ProgressBar<'a>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// let value = 50.0; +/// use iced::widget::progress_bar; /// -/// ProgressBar::new(0.0..=100.0, value); +/// struct State { +/// progress: f32, +/// } +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// progress_bar(0.0..=100.0, state.progress).into() +/// } /// ``` -/// -/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) #[allow(missing_debug_implementations)] pub struct ProgressBar<'a, Theme = crate::Theme> where From 1595e78b1ad58e09dfa049546f6627dd5f80075b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 05:05:09 +0200 Subject: [PATCH 324/657] Show `qr_code` doc example in multiple places --- widget/src/helpers.rs | 23 ++++++++++++++++++++++ widget/src/qr_code.rs | 44 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 53286e0a..4be5045a 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1290,8 +1290,31 @@ where /// Creates a new [`QRCode`] widget from the given [`Data`]. /// +/// QR codes display information in a type of two-dimensional matrix barcode. +/// /// [`QRCode`]: crate::QRCode /// [`Data`]: crate::qr_code::Data +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::qr_code; +/// +/// struct State { +/// data: qr_code::Data, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// qr_code(&state.data).into() +/// } +/// ``` #[cfg(feature = "qr_code")] pub fn qr_code<'a, Theme>( data: &'a crate::qr_code::Data, diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index e064aada..21dee6b1 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -1,4 +1,25 @@ -//! Encode and display information in a QR code. +//! QR codes display information in a type of two-dimensional matrix barcode. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::qr_code; +//! +//! struct State { +//! data: qr_code::Data, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! qr_code(&state.data).into() +//! } +//! ``` use crate::canvas; use crate::core::layout; use crate::core::mouse; @@ -18,6 +39,27 @@ const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which /// can be read by an imaging device, such as a camera. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::qr_code; +/// +/// struct State { +/// data: qr_code::Data, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// qr_code(&state.data).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct QRCode<'a, Theme = crate::Theme> where From b778d5cd567679619809b05e672398d3141558fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 05:14:17 +0200 Subject: [PATCH 325/657] Show `radio` doc example in multiple places --- widget/src/helpers.rs | 59 +++++++++++++++++- widget/src/radio.rs | 142 ++++++++++++++++++++++++++++++------------ 2 files changed, 160 insertions(+), 41 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 4be5045a..1e9bafa7 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -832,7 +832,64 @@ where /// Creates a new [`Radio`]. /// -/// [`Radio`]: crate::Radio +/// Radio buttons let users choose a single option from a bunch of options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::{column, radio}; +/// +/// struct State { +/// selection: Option, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// enum Message { +/// RadioSelected(Choice), +/// } +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// enum Choice { +/// A, +/// B, +/// C, +/// All, +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// let a = radio( +/// "A", +/// Choice::A, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let b = radio( +/// "B", +/// Choice::B, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let c = radio( +/// "C", +/// Choice::C, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let all = radio( +/// "All of the above", +/// Choice::All, +/// state.selection, +/// Message::RadioSelected +/// ); +/// +/// column![a, b, c, all].into() +/// } +/// ``` pub fn radio<'a, Message, Theme, Renderer, V>( label: impl Into, value: V, diff --git a/widget/src/radio.rs b/widget/src/radio.rs index cfa961f3..300318fd 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -1,4 +1,61 @@ -//! Create choices using radio buttons. +//! Radio buttons let users choose a single option from a bunch of options. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::{column, radio}; +//! +//! struct State { +//! selection: Option, +//! } +//! +//! #[derive(Debug, Clone, Copy)] +//! enum Message { +//! RadioSelected(Choice), +//! } +//! +//! #[derive(Debug, Clone, Copy, PartialEq, Eq)] +//! enum Choice { +//! A, +//! B, +//! C, +//! All, +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! let a = radio( +//! "A", +//! Choice::A, +//! state.selection, +//! Message::RadioSelected, +//! ); +//! +//! let b = radio( +//! "B", +//! Choice::B, +//! state.selection, +//! Message::RadioSelected, +//! ); +//! +//! let c = radio( +//! "C", +//! Choice::C, +//! state.selection, +//! Message::RadioSelected, +//! ); +//! +//! let all = radio( +//! "All of the above", +//! Choice::All, +//! state.selection, +//! Message::RadioSelected +//! ); +//! +//! column![a, b, c, all].into() +//! } +//! ``` use crate::core::alignment; use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; @@ -18,54 +75,59 @@ use crate::core::{ /// /// # Example /// ```no_run -/// # type Radio<'a, Message> = -/// # iced_widget::Radio<'a, Message, iced_widget::Theme, iced_widget::renderer::Renderer>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// # use iced_widget::column; +/// use iced::widget::{column, radio}; +/// +/// struct State { +/// selection: Option, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// enum Message { +/// RadioSelected(Choice), +/// } +/// /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// pub enum Choice { +/// enum Choice { /// A, /// B, /// C, /// All, /// } /// -/// #[derive(Debug, Clone, Copy)] -/// pub enum Message { -/// RadioSelected(Choice), +/// fn view(state: &State) -> Element<'_, Message> { +/// let a = radio( +/// "A", +/// Choice::A, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let b = radio( +/// "B", +/// Choice::B, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let c = radio( +/// "C", +/// Choice::C, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let all = radio( +/// "All of the above", +/// Choice::All, +/// state.selection, +/// Message::RadioSelected +/// ); +/// +/// column![a, b, c, all].into() /// } -/// -/// let selected_choice = Some(Choice::A); -/// -/// let a = Radio::new( -/// "A", -/// Choice::A, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let b = Radio::new( -/// "B", -/// Choice::B, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let c = Radio::new( -/// "C", -/// Choice::C, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let all = Radio::new( -/// "All of the above", -/// Choice::All, -/// selected_choice, -/// Message::RadioSelected -/// ); -/// -/// let content = column![a, b, c, all]; /// ``` #[allow(missing_debug_implementations)] pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> From 24fcc57873bdf2605c9df26d240d67d8e26873ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 05:19:54 +0200 Subject: [PATCH 326/657] Show `rule` doc example in multiple places --- widget/src/helpers.rs | 34 ++++++++++++++++++++++++++++++++-- widget/src/rule.rs | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1e9bafa7..40a72452 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1149,7 +1149,22 @@ pub fn vertical_space() -> Space { /// Creates a horizontal [`Rule`] with the given height. /// -/// [`Rule`]: crate::Rule +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::horizontal_rule; +/// +/// #[derive(Clone)] +/// enum Message { +/// // ..., +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// horizontal_rule(2).into() +/// } +/// ``` pub fn horizontal_rule<'a, Theme>(height: impl Into) -> Rule<'a, Theme> where Theme: rule::Catalog + 'a, @@ -1159,7 +1174,22 @@ where /// Creates a vertical [`Rule`] with the given width. /// -/// [`Rule`]: crate::Rule +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::vertical_rule; +/// +/// #[derive(Clone)] +/// enum Message { +/// // ..., +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// vertical_rule(2).into() +/// } +/// ``` pub fn vertical_rule<'a, Theme>(width: impl Into) -> Rule<'a, Theme> where Theme: rule::Catalog + 'a, diff --git a/widget/src/rule.rs b/widget/src/rule.rs index bbcd577e..92199ca9 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,4 +1,21 @@ -//! Display a horizontal or vertical rule for dividing content. +//! Rules divide space horizontally or vertically. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::horizontal_rule; +//! +//! #[derive(Clone)] +//! enum Message { +//! // ..., +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! horizontal_rule(2).into() +//! } +//! ``` use crate::core; use crate::core::border; use crate::core::layout; @@ -10,6 +27,23 @@ use crate::core::{ }; /// Display a horizontal or vertical rule for dividing content. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::horizontal_rule; +/// +/// #[derive(Clone)] +/// enum Message { +/// // ..., +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// horizontal_rule(2).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Rule<'a, Theme = crate::Theme> where From 94f0e0a2125faae7ea331f2edba99dd44b28a41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 05:30:12 +0200 Subject: [PATCH 327/657] Show `scrollable` doc example in multiple places --- widget/src/helpers.rs | 22 ++++++++++++++++++++- widget/src/scrollable.rs | 42 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 40a72452..9854c0bf 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -681,7 +681,27 @@ where /// Creates a new [`Scrollable`] with the provided content. /// -/// [`Scrollable`]: crate::Scrollable +/// Scrollables let users navigate an endless amount of content with a scrollbar. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{column, scrollable, vertical_space}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// scrollable(column![ +/// "Scroll me!", +/// vertical_space().height(3000), +/// "You did it!", +/// ]).into() +/// } +/// ``` pub fn scrollable<'a, Message, Theme, Renderer>( content: impl Into>, ) -> Scrollable<'a, Message, Theme, Renderer> diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index a5610166..01638a48 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,24 @@ -//! Navigate an endless amount of content with a scrollbar. +//! Scrollables let users navigate an endless amount of content with a scrollbar. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::{column, scrollable, vertical_space}; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! scrollable(column![ +//! "Scroll me!", +//! vertical_space().height(3000), +//! "You did it!", +//! ]).into() +//! } +//! ``` use crate::container; use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; @@ -24,6 +44,26 @@ pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{column, scrollable, vertical_space}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// scrollable(column![ +/// "Scroll me!", +/// vertical_space().height(3000), +/// "You did it!", +/// ]).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Scrollable< 'a, From 10fa40a85f09921c015fb2632aa34b9ef26d13cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 05:42:21 +0200 Subject: [PATCH 328/657] Show `slider` doc example in multiple places --- widget/src/helpers.rs | 62 +++++++++++++++++++++++++++++++++-- widget/src/slider.rs | 60 ++++++++++++++++++++++++++++----- widget/src/vertical_slider.rs | 58 ++++++++++++++++++++++++++++---- 3 files changed, 162 insertions(+), 18 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 9854c0bf..a8da2a32 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -969,7 +969,36 @@ where /// Creates a new [`Slider`]. /// -/// [`Slider`]: crate::Slider +/// Sliders let users set a value by moving an indicator. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::slider; +/// +/// struct State { +/// value: f32, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } +/// ``` pub fn slider<'a, T, Message, Theme>( range: std::ops::RangeInclusive, value: T, @@ -985,7 +1014,36 @@ where /// Creates a new [`VerticalSlider`]. /// -/// [`VerticalSlider`]: crate::VerticalSlider +/// Sliders let users set a value by moving an indicator. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::vertical_slider; +/// +/// struct State { +/// value: f32, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } +/// ``` pub fn vertical_slider<'a, T, Message, Theme>( range: std::ops::RangeInclusive, value: T, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 15514afe..9477958d 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -1,4 +1,33 @@ -//! Display an interactive selector of a single value from a range of values. +//! Sliders let users set a value by moving an indicator. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::slider; +//! +//! struct State { +//! value: f32, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! ValueChanged(f32), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! slider(0.0..=100.0, state.value, Message::ValueChanged).into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::ValueChanged(value) => { +//! state.value = value; +//! } +//! } +//! } +//! ``` use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -25,19 +54,32 @@ use std::ops::RangeInclusive; /// /// # Example /// ```no_run -/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), +/// use iced::widget::slider; +/// +/// struct State { +/// value: f32, /// } /// -/// let value = 50.0; +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } /// -/// Slider::new(0.0..=100.0, value, Message::SliderChanged); +/// fn view(state: &State) -> Element<'_, Message> { +/// slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } /// ``` -/// -/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) #[allow(missing_debug_implementations)] pub struct Slider<'a, T, Message, Theme = crate::Theme> where diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index a75ba49c..18633474 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -1,4 +1,33 @@ -//! Display an interactive selector of a single value from a range of values. +//! Sliders let users set a value by moving an indicator. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::slider; +//! +//! struct State { +//! value: f32, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! ValueChanged(f32), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! slider(0.0..=100.0, state.value, Message::ValueChanged).into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::ValueChanged(value) => { +//! state.value = value; +//! } +//! } +//! } +//! ``` use std::ops::RangeInclusive; pub use crate::slider::{ @@ -29,16 +58,31 @@ use crate::core::{ /// /// # Example /// ```no_run -/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), +/// use iced::widget::vertical_slider; +/// +/// struct State { +/// value: f32, /// } /// -/// let value = 50.0; +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } /// -/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); +/// fn view(state: &State) -> Element<'_, Message> { +/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } /// ``` #[allow(missing_debug_implementations)] pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> From 9773631354aa1af354647fbdbdd8111da2816afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 05:45:40 +0200 Subject: [PATCH 329/657] Show `svg` doc example in multiple places --- widget/src/helpers.rs | 18 ++++++++++++++++++ widget/src/svg.rs | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index a8da2a32..146393dc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1341,8 +1341,26 @@ pub fn image(handle: impl Into) -> crate::Image { /// Creates a new [`Svg`] widget from the given [`Handle`]. /// +/// Svg widgets display vector graphics in your application. +/// /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::svg; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// svg("tiger.svg").into() +/// } +/// ``` #[cfg(feature = "svg")] pub fn svg<'a, Theme>( handle: impl Into, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index bec0090f..8d57265a 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -1,4 +1,20 @@ -//! Display vector graphics in your application. +//! Svg widgets display vector graphics in your application. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::svg; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! svg("tiger.svg").into() +//! } +//! ``` use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -19,6 +35,22 @@ pub use crate::core::svg::Handle; /// /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::svg; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// svg("tiger.svg").into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Svg<'a, Theme = crate::Theme> where From 6ad7c7d3080816d37b17f1f6f5af5d8761983fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:10:44 +0200 Subject: [PATCH 330/657] Show `text` doc examples in multiple places --- core/src/widget/text.rs | 46 +++++++++++++++++++++++++++++++++++++++-- widget/src/helpers.rs | 46 +++++++++++++++++++++++++++++------------ widget/src/text.rs | 22 +++++++++++++++++++- 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index d8d6e4c6..8b02f8c2 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -1,4 +1,25 @@ -//! Write some text for your users to read. +//! Text widgets display information through writing. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub fn text(t: T) -> iced_core::widget::Text<'static, iced_core::Theme, ()> { unimplemented!() } } +//! # pub use iced_core::color; } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; +//! use iced::widget::text; +//! use iced::color; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! text("Hello, this is iced!") +//! .size(20) +//! .color(color!(0x0000ff)) +//! .into() +//! } +//! ``` use crate::alignment; use crate::layout; use crate::mouse; @@ -13,7 +34,28 @@ use crate::{ pub use text::{LineHeight, Shaping, Wrapping}; -/// A paragraph of text. +/// A bunch of text. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub fn text(t: T) -> iced_core::widget::Text<'static, iced_core::Theme, ()> { unimplemented!() } } +/// # pub use iced_core::color; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; +/// use iced::widget::text; +/// use iced::color; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text("Hello, this is iced!") +/// .size(20) +/// .color(color!(0x0000ff)) +/// .into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Text<'a, Theme, Renderer> where diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 146393dc..142d64d6 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -81,7 +81,6 @@ macro_rules! stack { /// /// ```no_run /// # mod iced { -/// # pub struct Element(pub std::marker::PhantomData); /// # pub mod widget { /// # macro_rules! text { /// # ($($arg:tt)*) => {unimplemented!()} @@ -89,22 +88,23 @@ macro_rules! stack { /// # pub(crate) use text; /// # } /// # } -/// # struct Example; -/// # enum Message {} -/// use iced::Element; +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::core::Theme, ()>; /// use iced::widget::text; /// -/// impl Example { -/// fn view(&self) -> Element { -/// let simple = text!("Hello, world!"); +/// enum Message { +/// // ... +/// } /// -/// let keyword = text!("Hello, {}", "world!"); +/// fn view(_state: &State) -> Element { +/// let simple = text!("Hello, world!"); /// -/// let planet = "Earth"; -/// let local_variable = text!("Hello, {planet}!"); -/// // ... -/// # iced::Element(std::marker::PhantomData) -/// } +/// let keyword = text!("Hello, {}", "world!"); +/// +/// let planet = "Earth"; +/// let local_variable = text!("Hello, {planet}!"); +/// // ... +/// # unimplemented!() /// } /// ``` #[macro_export] @@ -758,6 +758,26 @@ where } /// Creates a new [`Text`] widget with the provided content. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::core::Theme, ()>; +/// use iced::widget::text; +/// use iced::color; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text("Hello, this is iced!") +/// .size(20) +/// .color(color!(0x0000ff)) +/// .into() +/// } +/// ``` pub fn text<'a, Theme, Renderer>( text: impl text::IntoFragment<'a>, ) -> Text<'a, Theme, Renderer> diff --git a/widget/src/text.rs b/widget/src/text.rs index 9bf7fce4..c2243434 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -5,6 +5,26 @@ pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span}; pub use crate::core::widget::text::*; pub use rich::Rich; -/// A paragraph. +/// A bunch of text. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::text; +/// use iced::color; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text("Hello, this is iced!") +/// .size(20) +/// .color(color!(0x0000ff)) +/// .into() +/// } +/// ``` pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> = crate::core::widget::Text<'a, Theme, Renderer>; From 184ebebfe12c9ea68b91a0a5db59f8b599b5bc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:14:56 +0200 Subject: [PATCH 331/657] Show `text_editor` example in multiple places --- widget/src/helpers.rs | 34 +++++++++++++++++++- widget/src/text_editor.rs | 66 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 142d64d6..ca401a89 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -975,7 +975,39 @@ where /// Creates a new [`TextEditor`]. /// -/// [`TextEditor`]: crate::TextEditor +/// Text editors display a multi-line text input for text editing. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::text_editor; +/// +/// struct State { +/// content: text_editor::Content, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Edit(text_editor::Action) +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text_editor(&state.content) +/// .placeholder("Type something here...") +/// .on_action(Message::Edit) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::Edit(action) => { +/// state.content.perform(action); +/// } +/// } +/// } +/// ``` pub fn text_editor<'a, Message, Theme, Renderer>( content: &'a text_editor::Content, ) -> TextEditor<'a, core::text::highlighter::PlainText, Message, Theme, Renderer> diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 1df97962..59795318 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,4 +1,36 @@ -//! Display a multi-line text input for text editing. +//! Text editors display a multi-line text input for text editing. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::text_editor; +//! +//! struct State { +//! content: text_editor::Content, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Edit(text_editor::Action) +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! text_editor(&state.content) +//! .placeholder("Type something here...") +//! .on_action(Message::Edit) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::Edit(action) => { +//! state.content.perform(action); +//! } +//! } +//! } +//! ``` use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; use crate::core::event::{self, Event}; @@ -27,6 +59,38 @@ use std::sync::Arc; pub use text::editor::{Action, Edit, Motion}; /// A multi-line text input. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::text_editor; +/// +/// struct State { +/// content: text_editor::Content, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Edit(text_editor::Action) +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text_editor(&state.content) +/// .placeholder("Type something here...") +/// .on_action(Message::Edit) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::Edit(action) => { +/// state.content.perform(action); +/// } +/// } +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct TextEditor< 'a, From e0c55cbb19a6362b6a86bf71031ecaa26d673e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:18:00 +0200 Subject: [PATCH 332/657] Show `text_input` doc example in multiple places --- widget/src/helpers.rs | 33 ++++++++++++++++++- widget/src/text_input.rs | 68 +++++++++++++++++++++++++++++++--------- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index ca401a89..cf8c0520 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -960,7 +960,38 @@ where /// Creates a new [`TextInput`]. /// -/// [`TextInput`]: crate::TextInput +/// Text inputs display fields that can be filled with text. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::text_input; +/// +/// struct State { +/// content: String, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// ContentChanged(String) +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text_input("Type something here...", &state.content) +/// .on_input(Message::ContentChanged) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ContentChanged(content) => { +/// state.content = content; +/// } +/// } +/// } +/// ``` pub fn text_input<'a, Message, Theme, Renderer>( placeholder: &str, value: &str, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 3032dd13..5bbf76f5 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1,6 +1,35 @@ -//! Display fields that can be filled with text. +//! Text inputs display fields that can be filled with text. //! -//! A [`TextInput`] has some local [`State`]. +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::text_input; +//! +//! struct State { +//! content: String, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! ContentChanged(String) +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! text_input("Type something here...", &state.content) +//! .on_input(Message::ContentChanged) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::ContentChanged(content) => { +//! state.content = content; +//! } +//! } +//! } +//! ``` mod editor; mod value; @@ -38,23 +67,34 @@ use crate::runtime::Action; /// /// # Example /// ```no_run -/// # pub type TextInput<'a, Message> = iced_widget::TextInput<'a, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// #[derive(Debug, Clone)] -/// enum Message { -/// TextInputChanged(String), +/// use iced::widget::text_input; +/// +/// struct State { +/// content: String, /// } /// -/// let value = "Some text"; +/// #[derive(Debug, Clone)] +/// enum Message { +/// ContentChanged(String) +/// } /// -/// let input = TextInput::new( -/// "This is the placeholder...", -/// value, -/// ) -/// .on_input(Message::TextInputChanged) -/// .padding(10); +/// fn view(state: &State) -> Element<'_, Message> { +/// text_input("Type something here...", &state.content) +/// .on_input(Message::ContentChanged) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ContentChanged(content) => { +/// state.content = content; +/// } +/// } +/// } /// ``` -/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true) #[allow(missing_debug_implementations)] pub struct TextInput< 'a, From 22fbb9c2213892ae7981d5946b177a8846c4399e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:22:09 +0200 Subject: [PATCH 333/657] Show `toggler` doc example in multiple places --- widget/src/helpers.rs | 33 ++++++++++++++++++++++- widget/src/toggler.rs | 62 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index cf8c0520..ec4f2265 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -947,7 +947,38 @@ where /// Creates a new [`Toggler`]. /// -/// [`Toggler`]: crate::Toggler +/// Togglers let users make binary choices by toggling a switch. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::toggler; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { +/// TogglerToggled(bool), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// toggler(state.is_checked) +/// .label("Toggle me!") +/// .on_toggle(Message::TogglerToggled) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::TogglerToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } +/// ``` pub fn toggler<'a, Message, Theme, Renderer>( is_checked: bool, ) -> Toggler<'a, Message, Theme, Renderer> diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 1c425dc1..3b412081 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -1,4 +1,35 @@ -//! Show toggle controls using togglers. +//! Togglers let users make binary choices by toggling a switch. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::toggler; +//! +//! struct State { +//! is_checked: bool, +//! } +//! +//! enum Message { +//! TogglerToggled(bool), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! toggler(state.is_checked) +//! .label("Toggle me!") +//! .on_toggle(Message::TogglerToggled) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::TogglerToggled(is_checked) => { +//! state.is_checked = is_checked; +//! } +//! } +//! } +//! ``` use crate::core::alignment; use crate::core::event; use crate::core::layout; @@ -16,19 +47,34 @@ use crate::core::{ /// A toggler widget. /// /// # Example -/// /// ```no_run -/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// pub enum Message { +/// use iced::widget::toggler; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { /// TogglerToggled(bool), /// } /// -/// let is_toggled = true; +/// fn view(state: &State) -> Element<'_, Message> { +/// toggler(state.is_checked) +/// .label("Toggle me!") +/// .on_toggle(Message::TogglerToggled) +/// .into() +/// } /// -/// Toggler::new(is_toggled) -/// .label("Toggle me!") -/// .on_toggle(Message::TogglerToggled); +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::TogglerToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } /// ``` #[allow(missing_debug_implementations)] pub struct Toggler< From 4e38992636dded5868e74e5630a5da72bb4e5f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:27:54 +0200 Subject: [PATCH 334/657] Show `tooltip` doc example in multiple places --- widget/src/helpers.rs | 25 +++++++++++++++++++++-- widget/src/tooltip.rs | 46 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index ec4f2265..817d437c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -743,8 +743,29 @@ where /// Creates a new [`Tooltip`] for the provided content with the given /// [`Element`] and [`tooltip::Position`]. /// -/// [`Tooltip`]: crate::Tooltip -/// [`tooltip::Position`]: crate::tooltip::Position +/// Tooltips display a hint of information over some element when hovered. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{container, tooltip}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(_state: &State) -> Element<'_, Message> { +/// tooltip( +/// "Hover me to display the tooltip!", +/// container("This is the tooltip contents!") +/// .padding(10) +/// .style(container::rounded_box), +/// tooltip::Position::Bottom, +/// ).into() +/// } +/// ``` pub fn tooltip<'a, Message, Theme, Renderer>( content: impl Into>, tooltip: impl Into>, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 39f2e07d..e98f4da7 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -1,4 +1,26 @@ -//! Display a widget over another. +//! Tooltips display a hint of information over some element when hovered. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::{container, tooltip}; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(_state: &State) -> Element<'_, Message> { +//! tooltip( +//! "Hover me to display the tooltip!", +//! container("This is the tooltip contents!") +//! .padding(10) +//! .style(container::rounded_box), +//! tooltip::Position::Bottom, +//! ).into() +//! } +//! ``` use crate::container; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; @@ -13,6 +35,28 @@ use crate::core::{ }; /// An element to display a widget over another. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{container, tooltip}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(_state: &State) -> Element<'_, Message> { +/// tooltip( +/// "Hover me to display the tooltip!", +/// container("This is the tooltip contents!") +/// .padding(10) +/// .style(container::rounded_box), +/// tooltip::Position::Bottom, +/// ).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Tooltip< 'a, From cda1369c790cac27dce90abaead2bca940047ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:38:48 +0200 Subject: [PATCH 335/657] Write doc examples for `rich_text` widget --- widget/src/helpers.rs | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 817d437c..e17ef424 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -117,6 +117,31 @@ macro_rules! text { /// Creates some [`Rich`] text with the given spans. /// /// [`Rich`]: text::Rich +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::font; +/// use iced::widget::{rich_text, span}; +/// use iced::{color, Font}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// rich_text![ +/// span("I am red!").color(color!(0xff0000)), +/// " ", +/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }), +/// ] +/// .size(20) +/// .into() +/// } +/// ``` #[macro_export] macro_rules! rich_text { () => ( @@ -823,6 +848,31 @@ where /// Creates a new [`Rich`] text widget with the provided spans. /// /// [`Rich`]: text::Rich +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::font; +/// use iced::widget::{rich_text, span}; +/// use iced::{color, Font}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// rich_text([ +/// span("I am red!").color(color!(0xff0000)), +/// span(" "), +/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }), +/// ]) +/// .size(20) +/// .into() +/// } +/// ``` pub fn rich_text<'a, Link, Theme, Renderer>( spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a, ) -> text::Rich<'a, Link, Theme, Renderer> @@ -837,7 +887,35 @@ where /// Creates a new [`Span`] of text with the provided content. /// +/// A [`Span`] is a fragment of some [`Rich`] text. +/// /// [`Span`]: text::Span +/// [`Rich`]: text::Rich +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::font; +/// use iced::widget::{rich_text, span}; +/// use iced::{color, Font}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// rich_text![ +/// span("I am red!").color(color!(0xff0000)), +/// " ", +/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }), +/// ] +/// .size(20) +/// .into() +/// } +/// ``` pub fn span<'a, Link, Font>( text: impl text::IntoFragment<'a>, ) -> text::Span<'a, Link, Font> { From 31c42c1d02d6a76dedaa780e6832a23765c8aef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:49:22 +0200 Subject: [PATCH 336/657] Write doc examples for `column` and `row` --- widget/src/column.rs | 21 +++++++++++ widget/src/helpers.rs | 84 ++++++++++++++++++++++++++++++++++++++++--- widget/src/row.rs | 21 +++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/widget/src/column.rs b/widget/src/column.rs index d3ea4cf7..fc4653b9 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -12,6 +12,27 @@ use crate::core::{ }; /// A container that distributes its contents vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, column}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column![ +/// "I am on top!", +/// button("I am in the center!"), +/// "I am below.", +/// ].into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e17ef424..52290a54 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -31,7 +31,28 @@ use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. /// -/// [`Column`]: crate::Column +/// Columns distribute their children vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, column}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column![ +/// "I am on top!", +/// button("I am in the center!"), +/// "I am below.", +/// ].into() +/// } +/// ``` #[macro_export] macro_rules! column { () => ( @@ -44,7 +65,28 @@ macro_rules! column { /// Creates a [`Row`] with the given children. /// -/// [`Row`]: crate::Row +/// Rows distribute their children horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, row}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row![ +/// "I am to the left!", +/// button("I am in the middle!"), +/// "I am to the right!", +/// ].into() +/// } +/// ``` #[macro_export] macro_rules! row { () => ( @@ -208,6 +250,24 @@ where } /// Creates a new [`Column`] with the given children. +/// +/// Columns distribute their children vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{column, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column((0..5).map(|i| text!("Item {i}").into())).into() +/// } +/// ``` pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator>, ) -> Column<'a, Message, Theme, Renderer> @@ -248,9 +308,25 @@ where keyed::Column::with_children(children) } -/// Creates a new [`Row`] with the given children. +/// Creates a new [`Row`] from an iterator. /// -/// [`Row`]: crate::Row +/// Rows distribute their children horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{row, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row((0..5).map(|i| text!("Item {i}").into())).into() +/// } +/// ``` pub fn row<'a, Message, Theme, Renderer>( children: impl IntoIterator>, ) -> Row<'a, Message, Theme, Renderer> diff --git a/widget/src/row.rs b/widget/src/row.rs index 85af912f..fbb3f066 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -12,6 +12,27 @@ use crate::core::{ }; /// A container that distributes its contents horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, row}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row![ +/// "I am to the left!", +/// button("I am in the middle!"), +/// "I am to the right!", +/// ].into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { spacing: f32, From 1f8dc1f3dda25c699b94c653d5d569f4142e9b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 06:56:19 +0200 Subject: [PATCH 337/657] Fix `mouse_area` not notifying of mouse move events --- widget/src/mouse_area.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index d255ac99..7330874a 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -316,7 +316,7 @@ fn update( let cursor_position = cursor.position(); let bounds = layout.bounds(); - if state.cursor_position != cursor_position && state.bounds != bounds { + if state.cursor_position != cursor_position || state.bounds != bounds { let was_hovered = state.is_hovered; state.is_hovered = cursor.is_over(layout.bounds()); From bf3b6f100df7b1585dfac88da432bc29784ed534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 07:05:51 +0200 Subject: [PATCH 338/657] Bump version to `0.13.1` :tada: --- CHANGELOG.md | 20 +++++++++++++++++++- Cargo.toml | 24 ++++++++++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6addcc69..fff7d341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.1] - 2024-09-19 +### Added +- Some `From` trait implementations for `text_input::Id`. [#2582](https://github.com/iced-rs/iced/pull/2582) +- Custom `Executor` support for `Application` and `Daemon`. [#2580](https://github.com/iced-rs/iced/pull/2580) +- `rust-version` metadata to `Cargo.toml`. [#2579](https://github.com/iced-rs/iced/pull/2579) +- Widget examples to API reference. [#2587](https://github.com/iced-rs/iced/pull/2587) + +### Fixed +- Inverted scrolling direction with trackpad in `scrollable`. [#2583](https://github.com/iced-rs/iced/pull/2583) +- `scrollable` transactions when `on_scroll` is not set. [#2584](https://github.com/iced-rs/iced/pull/2584) +- Incorrect text color styling in `text_editor` widget. [#2586](https://github.com/iced-rs/iced/pull/2586) + +Many thanks to... +- @dcampbell24 +- @lufte +- @mtkennerly + ## [0.13.0] - 2024-09-18 ### Added - Introductory chapters to the [official guide book](https://book.iced.rs/). @@ -971,7 +988,8 @@ Many thanks to... ### Added - First release! :tada: -[Unreleased]: https://github.com/iced-rs/iced/compare/0.13.0...HEAD +[Unreleased]: https://github.com/iced-rs/iced/compare/0.13.1...HEAD +[0.13.1]: https://github.com/iced-rs/iced/compare/0.13.0...0.13.1 [0.13.0]: https://github.com/iced-rs/iced/compare/0.12.1...0.13.0 [0.12.1]: https://github.com/iced-rs/iced/compare/0.12.0...0.12.1 [0.12.0]: https://github.com/iced-rs/iced/compare/0.10.0...0.12.0 diff --git a/Cargo.toml b/Cargo.toml index df087fdf..52cd4c49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ members = [ ] [workspace.package] -version = "0.13.0" +version = "0.13.1" authors = ["Héctor Ramón Jiménez "] edition = "2021" license = "MIT" @@ -128,17 +128,17 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] rust-version = "1.80" [workspace.dependencies] -iced = { version = "0.13.0", path = "." } -iced_core = { version = "0.13.0", path = "core" } -iced_futures = { version = "0.13.0", path = "futures" } -iced_graphics = { version = "0.13.0", path = "graphics" } -iced_highlighter = { version = "0.13.0", path = "highlighter" } -iced_renderer = { version = "0.13.0", path = "renderer" } -iced_runtime = { version = "0.13.0", path = "runtime" } -iced_tiny_skia = { version = "0.13.0", path = "tiny_skia" } -iced_wgpu = { version = "0.13.0", path = "wgpu" } -iced_widget = { version = "0.13.0", path = "widget" } -iced_winit = { version = "0.13.0", path = "winit" } +iced = { version = "0.13", path = "." } +iced_core = { version = "0.13", path = "core" } +iced_futures = { version = "0.13", path = "futures" } +iced_graphics = { version = "0.13", path = "graphics" } +iced_highlighter = { version = "0.13", path = "highlighter" } +iced_renderer = { version = "0.13", path = "renderer" } +iced_runtime = { version = "0.13", path = "runtime" } +iced_tiny_skia = { version = "0.13", path = "tiny_skia" } +iced_wgpu = { version = "0.13", path = "wgpu" } +iced_widget = { version = "0.13", path = "widget" } +iced_winit = { version = "0.13", path = "winit" } async-std = "1.0" bitflags = "2.0" From 114f7dfa14530a0197125dfef36d2a385bff5c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 23:17:09 +0200 Subject: [PATCH 339/657] Add `must_use` attribute to `Task` --- futures/src/subscription.rs | 2 +- runtime/src/task.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 8067c259..eaea1a1f 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -113,7 +113,7 @@ pub type Hasher = rustc_hash::FxHasher; /// ``` /// /// [`Future`]: std::future::Future -#[must_use = "`Subscription` must be returned to runtime to take effect"] +#[must_use = "`Subscription` must be returned to the runtime to take effect; normally in your `subscription` function."] pub struct Subscription { recipes: Vec>>, } diff --git a/runtime/src/task.rs b/runtime/src/task.rs index ec8d7cc7..3f97d134 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -14,6 +14,7 @@ use std::future::Future; /// /// 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(Option>>); impl Task { From fed9c8d19bed572aec80376722fc5ef0d48ac417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 19 Sep 2024 23:36:05 +0200 Subject: [PATCH 340/657] Bump version to `0.14.0-dev` :tada: --- Cargo.toml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52cd4c49..6de4f4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ members = [ ] [workspace.package] -version = "0.13.1" +version = "0.14.0-dev" authors = ["Héctor Ramón Jiménez "] edition = "2021" license = "MIT" @@ -128,17 +128,17 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] rust-version = "1.80" [workspace.dependencies] -iced = { version = "0.13", path = "." } -iced_core = { version = "0.13", path = "core" } -iced_futures = { version = "0.13", path = "futures" } -iced_graphics = { version = "0.13", path = "graphics" } -iced_highlighter = { version = "0.13", path = "highlighter" } -iced_renderer = { version = "0.13", path = "renderer" } -iced_runtime = { version = "0.13", path = "runtime" } -iced_tiny_skia = { version = "0.13", path = "tiny_skia" } -iced_wgpu = { version = "0.13", path = "wgpu" } -iced_widget = { version = "0.13", path = "widget" } -iced_winit = { version = "0.13", path = "winit" } +iced = { version = "0.14.0-dev", path = "." } +iced_core = { version = "0.14.0-dev", path = "core" } +iced_futures = { version = "0.14.0-dev", path = "futures" } +iced_graphics = { version = "0.14.0-dev", path = "graphics" } +iced_highlighter = { version = "0.14.0-dev", path = "highlighter" } +iced_renderer = { version = "0.14.0-dev", path = "renderer" } +iced_runtime = { version = "0.14.0-dev", path = "runtime" } +iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } +iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } +iced_widget = { version = "0.14.0-dev", path = "widget" } +iced_winit = { version = "0.14.0-dev", path = "winit" } async-std = "1.0" bitflags = "2.0" From aed9a03e3cc30c68488f6e177e2ea0513e7ffc99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 1 May 2024 16:19:08 +0200 Subject: [PATCH 341/657] Update `wgpu` to `0.20.1` --- Cargo.toml | 4 ++-- examples/custom_shader/src/scene/pipeline.rs | 8 ++++++++ examples/integration/src/scene.rs | 2 ++ wgpu/src/color.rs | 4 ++++ wgpu/src/image/mod.rs | 4 ++++ wgpu/src/quad/gradient.rs | 4 ++++ wgpu/src/quad/solid.rs | 4 ++++ wgpu/src/triangle.rs | 8 ++++++++ wgpu/src/triangle/msaa.rs | 4 ++++ 9 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6de4f4bf..000ca6ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,7 +148,7 @@ cosmic-text = "0.12" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "feef9f5630c2adb3528937e55f7bfad2da561a65" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "ce412b3954118d2a4ae20de2d6959247d6f7ed76" } guillotiere = "0.6" half = "2.2" image = { version = "0.24", default-features = false } @@ -181,7 +181,7 @@ wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3.69" web-time = "1.1" -wgpu = "0.19" +wgpu = "0.20.1" winapi = "0.3" window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 50b70a98..20ca6a67 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -243,6 +243,8 @@ impl Pipeline { module: &shader, entry_point: "vs_main", buffers: &[Vertex::desc(), cube::Raw::desc()], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState::default(), depth_stencil: Some(wgpu::DepthStencilState { @@ -276,6 +278,8 @@ impl Pipeline { }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), multiview: None, }); @@ -490,6 +494,8 @@ impl DepthPipeline { module: &shader, entry_point: "vs_main", buffers: &[], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, primitive: wgpu::PrimitiveState::default(), depth_stencil: Some(wgpu::DepthStencilState { @@ -508,6 +514,8 @@ impl DepthPipeline { blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), multiview: None, }); diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index e29558bf..f3a7a194 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -74,6 +74,7 @@ fn build_pipeline( module: &vs_module, entry_point: "main", buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &fs_module, @@ -86,6 +87,7 @@ fn build_pipeline( }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 9d593d9c..f5c4af30 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -110,6 +110,8 @@ pub fn convert( module: &shader, entry_point: "vs_main", buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default( + ), }, fragment: Some(wgpu::FragmentState { module: &shader, @@ -130,6 +132,8 @@ pub fn convert( }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: wgpu::PipelineCompilationOptions::default( + ), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 1b16022a..d72303ea 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -153,6 +153,8 @@ impl Pipeline { 8 => Uint32, ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, @@ -173,6 +175,8 @@ impl Pipeline { }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 13dc10f8..b915a6b7 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -152,11 +152,15 @@ impl Pipeline { 9 => Float32 ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "gradient_fs_main", targets: &quad::color_target_state(format), + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 45039a2d..060ef881 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -114,11 +114,15 @@ impl Pipeline { 8 => Float32, ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "solid_fs_main", targets: &quad::color_target_state(format), + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index b0551f55..0a2a2174 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -760,11 +760,15 @@ mod solid { 1 => Float32x4, ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "solid_fs_main", targets: &[Some(triangle::fragment_target(format))], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: triangle::primitive_state(), depth_stencil: None, @@ -937,11 +941,15 @@ mod gradient { 6 => Float32x4 ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "gradient_fs_main", targets: &[Some(triangle::fragment_target(format))], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: triangle::primitive_state(), depth_stencil: None, diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 71c16925..1f230b71 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -112,6 +112,8 @@ impl Blit { module: &shader, entry_point: "vs_main", buffers: &[], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, @@ -123,6 +125,8 @@ impl Blit { ), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, From a5e69cfb5f7856e4d139ef58e5d022b9d408b542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 19 Jul 2024 19:10:23 +0200 Subject: [PATCH 342/657] Update `wgpu` to `22.0` --- Cargo.toml | 4 ++-- benches/wgpu.rs | 1 + examples/custom_shader/src/scene/pipeline.rs | 2 ++ examples/integration/src/main.rs | 2 ++ examples/integration/src/scene.rs | 1 + wgpu/src/color.rs | 1 + wgpu/src/image/mod.rs | 1 + wgpu/src/quad/gradient.rs | 1 + wgpu/src/quad/solid.rs | 1 + wgpu/src/triangle.rs | 2 ++ wgpu/src/triangle/msaa.rs | 1 + wgpu/src/window/compositor.rs | 1 + 12 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 000ca6ba..629b1c9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,7 +148,7 @@ cosmic-text = "0.12" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "ce412b3954118d2a4ae20de2d6959247d6f7ed76" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "0d7ba1bba4dd71eb88d2cface5ce649db2413cb7" } guillotiere = "0.6" half = "2.2" image = { version = "0.24", default-features = false } @@ -181,7 +181,7 @@ wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3.69" web-time = "1.1" -wgpu = "0.20.1" +wgpu = "22.0" winapi = "0.3" window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 0e407253..0605294f 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -66,6 +66,7 @@ fn benchmark<'a>( label: None, required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), + memory_hints: wgpu::MemoryHints::MemoryUsage, }, None, )) diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 20ca6a67..84a3e5e2 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -282,6 +282,7 @@ impl Pipeline { wgpu::PipelineCompilationOptions::default(), }), multiview: None, + cache: None, }); let depth_pipeline = DepthPipeline::new( @@ -518,6 +519,7 @@ impl DepthPipeline { wgpu::PipelineCompilationOptions::default(), }), multiview: None, + cache: None, }); Self { diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 5b64cbd1..87a5b22b 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -102,6 +102,8 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { required_features: adapter_features & wgpu::Features::default(), required_limits: wgpu::Limits::default(), + memory_hints: + wgpu::MemoryHints::MemoryUsage, }, None, ) diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index f3a7a194..15f97e08 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -101,5 +101,6 @@ fn build_pipeline( alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }) } diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index f5c4af30..effac8da 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -143,6 +143,7 @@ pub fn convert( depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, + cache: None, }); let texture = device.create_texture(&wgpu::TextureDescriptor { diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index d72303ea..cf83c3f2 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -190,6 +190,7 @@ impl Pipeline { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }); Pipeline { diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index b915a6b7..207b0d73 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -174,6 +174,7 @@ impl Pipeline { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }, ); diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 060ef881..86f118d6 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -136,6 +136,7 @@ impl Pipeline { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }); Self { pipeline } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 0a2a2174..3d0869e7 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -774,6 +774,7 @@ mod solid { depth_stencil: None, multisample: triangle::multisample_state(antialiasing), multiview: None, + cache: None, }, ); @@ -955,6 +956,7 @@ mod gradient { depth_stencil: None, multisample: triangle::multisample_state(antialiasing), multiview: None, + cache: None, }, ); diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 1f230b71..ec06e747 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -140,6 +140,7 @@ impl Blit { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }); Blit { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 2e938c77..0cfd49b7 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -162,6 +162,7 @@ impl Compositor { ), required_features: wgpu::Features::empty(), required_limits: required_limits.clone(), + memory_hints: wgpu::MemoryHints::MemoryUsage, }, None, ) From 84b658dbef0b29c57f67e041a1496c699ce78615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 20 Sep 2024 00:39:21 +0200 Subject: [PATCH 343/657] Introduce `strict-assertions` feature flag For now, this feature flag only enables validation in `iced_wgpu`; which has become quite expensive since its `0.20` release. --- Cargo.toml | 2 ++ renderer/Cargo.toml | 1 + wgpu/Cargo.toml | 1 + wgpu/src/window/compositor.rs | 5 +++++ 4 files changed, 9 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 629b1c9d..bee83d2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,8 @@ advanced = ["iced_core/advanced", "iced_widget/advanced"] fira-sans = ["iced_renderer/fira-sans"] # Enables auto-detecting light/dark mode for the built-in theme auto-detect-theme = ["iced_core/auto-detect-theme"] +# Enables strict assertions for debugging purposes at the expense of performance +strict-assertions = ["iced_renderer/strict-assertions"] [dependencies] iced_core.workspace = true diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 458681dd..ac223f16 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -22,6 +22,7 @@ geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/ge web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] +strict-assertions = ["iced_wgpu?/strict-assertions"] [dependencies] iced_graphics.workspace = true diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index b13ecb36..a8ebf3aa 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -23,6 +23,7 @@ image = ["iced_graphics/image"] svg = ["iced_graphics/svg", "resvg/text"] web-colors = ["iced_graphics/web-colors"] webgl = ["wgpu/webgl"] +strict-assertions = [] [dependencies] iced_graphics.workspace = true diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 0cfd49b7..56f33b50 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -56,6 +56,11 @@ impl Compositor { ) -> Result { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.backends, + flags: if cfg!(feature = "strict-assertions") { + wgpu::InstanceFlags::debugging() + } else { + wgpu::InstanceFlags::empty() + }, ..Default::default() }); From 2a547ae372053be776b5881ec217402d38768c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 20 Sep 2024 17:27:43 +0200 Subject: [PATCH 344/657] Drop short-hand notation support for `color!` macro We'd need to use `stringify!` and `str::len` to properly support the short-hand notation; however, we want the macro to work in `const` contexts. --- core/src/color.rs | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index 46fe9ecd..827d0289 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -229,13 +229,12 @@ impl From<[f32; 4]> for Color { /// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT); /// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0)); /// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0)); -/// assert_eq!(color!(0x123), Color::from_rgba8(0x11, 0x22, 0x33, 1.0)); -/// assert_eq!(color!(0x123), color!(0x112233)); +/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0)); /// ``` #[macro_export] macro_rules! color { ($r:expr, $g:expr, $b:expr) => { - color!($r, $g, $b, 1.0) + $crate::color!($r, $g, $b, 1.0) }; ($r:expr, $g:expr, $b:expr, $a:expr) => {{ let r = $r as f32 / 255.0; @@ -261,29 +260,18 @@ macro_rules! color { $crate::Color { r, g, b, a: $a } }}; ($hex:expr) => {{ - color!($hex, 1.0) + $crate::color!($hex, 1.0) }}; ($hex:expr, $a:expr) => {{ let hex = $hex as u32; - if hex <= 0xfff { - let r = (hex & 0xf00) >> 8; - let g = (hex & 0x0f0) >> 4; - let b = (hex & 0x00f); + debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff"); - color!((r << 4 | r), (g << 4 | g), (b << 4 | b), $a) - } else { - debug_assert!( - hex <= 0xffffff, - "color! value must not exceed 0xffffff" - ); + let r = (hex & 0xff0000) >> 16; + let g = (hex & 0xff00) >> 8; + let b = (hex & 0xff); - let r = (hex & 0xff0000) >> 16; - let g = (hex & 0xff00) >> 8; - let b = (hex & 0xff); - - color!(r, g, b, $a) - } + $crate::color!(r, g, b, $a) }}; } From 91c00e4abf0a21c2b10535a1672e017249cfbcee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 20 Sep 2024 18:35:18 +0200 Subject: [PATCH 345/657] Move `wgpu` re-export to root module This seems to fix a `cargo doc` performance issue; and it makes more sense anyways. --- examples/custom_shader/src/main.rs | 2 +- src/lib.rs | 3 +++ widget/src/shader.rs | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 5886f6bb..8c187d3c 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -3,7 +3,7 @@ mod scene; use scene::Scene; use iced::time::Instant; -use iced::widget::shader::wgpu; +use iced::wgpu; use iced::widget::{center, checkbox, column, row, shader, slider, text}; use iced::window; use iced::{Center, Color, Element, Fill, Subscription}; diff --git a/src/lib.rs b/src/lib.rs index 91d8e53f..8f526cfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -483,6 +483,9 @@ pub use iced_futures::stream; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; +#[cfg(feature = "wgpu")] +pub use iced_renderer::wgpu::wgpu; + mod error; mod program; diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 3c81f8ed..fa692336 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -18,7 +18,6 @@ use crate::renderer::wgpu::primitive; use std::marker::PhantomData; pub use crate::graphics::Viewport; -pub use crate::renderer::wgpu::wgpu; pub use primitive::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. From e9aa276a936f37afe16376bbe58306ee502bb771 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Fri, 20 Sep 2024 15:57:09 -0500 Subject: [PATCH 346/657] Fix documentation for open_events() --- runtime/src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index cdf3d80a..27035d7d 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -187,7 +187,7 @@ pub fn events() -> Subscription<(Id, Event)> { }) } -/// Subscribes to all [`Event::Closed`] occurrences in the running application. +/// Subscribes to all [`Event::Opened`] occurrences in the running application. pub fn open_events() -> Subscription { event::listen_with(|event, _status, id| { if let crate::core::Event::Window(Event::Opened { .. }) = event { From 05d5e0739da9c8f8900bcfd45e1cb7715562be29 Mon Sep 17 00:00:00 2001 From: mtkennerly Date: Fri, 20 Sep 2024 22:15:03 -0400 Subject: [PATCH 347/657] Fix layout for wrapped row with spacing --- widget/src/row.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/row.rs b/widget/src/row.rs index fbb3f066..75d5fb40 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -477,7 +477,7 @@ where intrinsic_size.width = intrinsic_size.width.max(x - spacing); } - intrinsic_size.height = (y - spacing).max(0.0) + row_height; + intrinsic_size.height = y + row_height; align(row_start..children.len(), row_height, &mut children); let size = From 1383c6a4f700ff246148c913000e9b9368ea9afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 21 Sep 2024 21:14:54 +0200 Subject: [PATCH 348/657] Fix flex layout of `Fill` elements in a `Shrink` cross axis Instead of collapsing, the `Fill` elements will fill the cross space allocated by the other `Shrink` elements present in the container. --- core/src/layout/flex.rs | 13 ++++++------ examples/layout/src/main.rs | 40 ++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index dcb4d8de..ac80d393 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -79,10 +79,10 @@ where let max_cross = axis.cross(limits.max()); let mut fill_main_sum = 0; - let mut cross = match axis { - Axis::Vertical if width == Length::Shrink => 0.0, - Axis::Horizontal if height == Length::Shrink => 0.0, - _ => max_cross, + let (mut cross, cross_compress) = match axis { + Axis::Vertical if width == Length::Shrink => (0.0, true), + Axis::Horizontal if height == Length::Shrink => (0.0, true), + _ => (max_cross, false), }; let mut available = axis.main(limits.max()) - total_spacing; @@ -97,7 +97,8 @@ where axis.pack(size.width.fill_factor(), size.height.fill_factor()) }; - if fill_main_factor == 0 { + if fill_main_factor == 0 && (!cross_compress || fill_cross_factor == 0) + { let (max_width, max_height) = axis.pack( available, if fill_cross_factor == 0 { @@ -141,7 +142,7 @@ where axis.pack(size.width.fill_factor(), size.height.fill_factor()) }; - if fill_main_factor != 0 { + if fill_main_factor != 0 || (cross_compress && fill_cross_factor != 0) { let max_main = remaining * fill_main_factor as f32 / fill_main_sum as f32; diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index cb33369b..4280a003 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -2,12 +2,12 @@ use iced::border; use iced::keyboard; use iced::mouse; use iced::widget::{ - button, canvas, center, checkbox, column, container, horizontal_space, - pick_list, row, scrollable, text, + button, canvas, center, checkbox, column, container, horizontal_rule, + horizontal_space, pick_list, row, scrollable, text, vertical_rule, }; use iced::{ color, Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, - Subscription, Theme, + Shrink, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -147,6 +147,10 @@ impl Example { title: "Application", view: application, }, + Self { + title: "Quotes", + view: quotes, + }, ]; fn is_first(self) -> bool { @@ -275,6 +279,36 @@ fn application<'a>() -> Element<'a, Message> { column![header, row![sidebar, content]].into() } +fn quotes<'a>() -> Element<'a, Message> { + fn quote<'a>( + content: impl Into>, + ) -> Element<'a, Message> { + row![vertical_rule(2), content.into()] + .spacing(10) + .height(Shrink) + .into() + } + + fn reply<'a>( + original: impl Into>, + reply: impl Into>, + ) -> Element<'a, Message> { + column![quote(original), reply.into()].spacing(10).into() + } + + column![ + reply( + reply("This is the original message", "This is a reply"), + "This is another reply", + ), + horizontal_rule(1), + "A separator ↑", + ] + .width(Shrink) + .spacing(10) + .into() +} + fn square<'a>(size: impl Into + Copy) -> Element<'a, Message> { struct Square; From 24150effad51d6962d00be0950fef4cbac8e6ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 21 Sep 2024 21:20:20 +0200 Subject: [PATCH 349/657] Remove broken links to `ECOSYSTEM.md` --- CONTRIBUTING.md | 3 +-- ROADMAP.md | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e7075c6..96302a65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Thank you for considering contributing to Iced! Feel free to read [the ecosystem overview] and [the roadmap] to get an idea of the current state of the library. +Thank you for considering contributing to Iced! Take a look at [the roadmap] to get an idea of the current state of the library. The core team is busy and does not have time to mentor nor babysit new contributors. If a member of the core team thinks that reviewing and understanding your work will take more time and effort than writing it from scratch by themselves, your contribution will be dismissed. It is your responsibility to communicate and figure out how to reduce the likelihood of this! @@ -15,7 +15,6 @@ Besides directly writing code, there are many other different ways you can contr - Submitting bug reports and use cases - Sharing, discussing, researching and exploring new ideas or crates -[the ecosystem overview]: ECOSYSTEM.md [the roadmap]: ROADMAP.md [our Discourse forum]: https://discourse.iced.rs/ [Code is the Easy Part]: https://youtu.be/DSjbTC-hvqQ?t=1138 diff --git a/ROADMAP.md b/ROADMAP.md index afcece7c..a7f3b677 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,2 @@ # Roadmap We have [a detailed graphical roadmap now](https://whimsical.com/roadmap-iced-7vhq6R35Lp3TmYH4WeYwLM)! - -Before diving into the roadmap, check out [the ecosystem overview] to get an idea of the current state of the library. - -[the ecosystem overview]: ECOSYSTEM.md From 6d1ecb79e38ece36420278494d1a5b2b3062161b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 21 Sep 2024 22:27:49 +0200 Subject: [PATCH 350/657] Replace `Rc` with `Arc` for `markdown` caching --- widget/src/markdown.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 81bea0c5..8adc368c 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -57,7 +57,7 @@ use crate::core::{ use crate::{column, container, rich_text, row, scrollable, span, text}; use std::cell::{Cell, RefCell}; -use std::rc::Rc; +use std::sync::Arc; pub use core::text::Highlight; pub use pulldown_cmark::HeadingLevel; @@ -88,7 +88,7 @@ pub enum Item { pub struct Text { spans: Vec, last_style: Cell>, - last_styled_spans: RefCell]>>, + last_styled_spans: RefCell]>>, } impl Text { @@ -104,7 +104,7 @@ impl Text { /// /// This method performs caching for you. It will only reallocate if the [`Style`] /// provided changes. - pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> { + pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Url>]> { if Some(style) != self.last_style.get() { *self.last_styled_spans.borrow_mut() = self.spans.iter().map(|span| span.view(&style)).collect(); From f984e759eb409762ea8c7c5d75f6051740373e16 Mon Sep 17 00:00:00 2001 From: edwloef Date: Sun, 22 Sep 2024 12:21:57 +0200 Subject: [PATCH 351/657] always increment solid/gradient count in wgpu mesh rendering --- wgpu/src/triangle.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 3d0869e7..fb858c10 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -505,6 +505,14 @@ impl Layer { .intersection(&(mesh.clip_bounds() * transformation)) .and_then(Rectangle::snap) else { + match mesh { + Mesh::Solid { .. } => { + num_solids += 1; + } + Mesh::Gradient { .. } => { + num_gradients += 1; + } + } continue; }; From 26b09e1b4d13f866a0195305c9e781da27a9b483 Mon Sep 17 00:00:00 2001 From: Gabriel Vogel Date: Tue, 24 Sep 2024 22:29:03 +0200 Subject: [PATCH 352/657] Include images and saved meshes when pasting `Frame` `tiny_skia::Frame` was ignoring images in `Frame::paste`, making images not show up when created in a `with_clip` context. `wgpu::Frame` similarly did not pass through meshes in its paste method, that may have been saved from a nested `with_clip` call. --- tiny_skia/src/geometry.rs | 1 + wgpu/src/geometry.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 0d5fff62..681bf25d 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -256,6 +256,7 @@ impl geometry::frame::Backend for Frame { fn paste(&mut self, frame: Self) { self.primitives.extend(frame.primitives); self.text.extend(frame.text); + self.images.extend(frame.images); } fn translate(&mut self, translation: Vector) { diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 8e6f77d7..a27cef07 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -395,6 +395,7 @@ impl geometry::frame::Backend for Frame { } fn paste(&mut self, frame: Frame) { + self.meshes.extend(frame.meshes); self.meshes .extend(frame.buffers.into_meshes(frame.clip_bounds)); From 75548373a761d66df364494267c89697dda91fbe Mon Sep 17 00:00:00 2001 From: 7sDream Date: Wed, 25 Sep 2024 05:05:17 +0800 Subject: [PATCH 353/657] Add support for double click event to MouseArea (#2602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(widget/mouse_area): add double_click event * Run `cargo fmt` --------- Co-authored-by: Héctor Ramón Jiménez --- widget/src/mouse_area.rs | 54 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 7330874a..c5a37ae3 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -22,6 +22,7 @@ pub struct MouseArea< content: Element<'a, Message, Theme, Renderer>, on_press: Option, on_release: Option, + on_double_click: Option, on_right_press: Option, on_right_release: Option, on_middle_press: Option, @@ -48,6 +49,22 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self } + /// The message to emit on a double click. + /// + /// If you use this with [`on_press`]/[`on_release`], those + /// event will be emit as normal. + /// + /// The events stream will be: on_press -> on_release -> on_press + /// -> on_double_click -> on_release -> on_press ... + /// + /// [`on_press`]: Self::on_press + /// [`on_release`]: Self::on_release + #[must_use] + pub fn on_double_click(mut self, message: Message) -> Self { + self.on_double_click = Some(message); + self + } + /// The message to emit on a right button press. #[must_use] pub fn on_right_press(mut self, message: Message) -> Self { @@ -121,6 +138,7 @@ struct State { is_hovered: bool, bounds: Rectangle, cursor_position: Option, + previous_click: Option, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -132,6 +150,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { content: content.into(), on_press: None, on_release: None, + on_double_click: None, on_right_press: None, on_right_release: None, on_middle_press: None, @@ -347,12 +366,37 @@ fn update( return event::Status::Ignored; } - if let Some(message) = widget.on_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { - shell.publish(message.clone()); + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + let mut captured = false; + if let Some(message) = widget.on_press.as_ref() { + captured = true; + shell.publish(message.clone()); + } + + if let Some(position) = cursor_position { + if let Some(message) = widget.on_double_click.as_ref() { + let new_click = mouse::Click::new( + position, + mouse::Button::Left, + state.previous_click, + ); + + if matches!(new_click.kind(), mouse::click::Kind::Double) { + shell.publish(message.clone()); + } + + state.previous_click = Some(new_click); + + // Even if this is not a double click, but the press is nevertheless + // processed by us and should not be popup to parent widgets. + captured = true; + } + } + + if captured { return event::Status::Captured; } } From a949e59f3e2125e33ce7ebe904d6a98711c1fedf Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Tue, 24 Sep 2024 20:57:53 -0500 Subject: [PATCH 354/657] feat: set total size of QRCode --- examples/qr_code/src/main.rs | 48 ++++++++++++++++++++++++++++++++---- widget/src/qr_code.rs | 16 ++++++++---- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index f1b654e0..638f31c0 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,6 +1,12 @@ -use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; +use iced::widget::{ + center, column, pick_list, qr_code, row, slider, text, text_input, toggler, +}; use iced::{Center, Element, Theme}; +const QR_CODE_EXACT_SIZE_MIN_PX: u32 = 200; +const QR_CODE_EXACT_SIZE_MAX_PX: u32 = 400; +const QR_CODE_EXACT_SIZE_SLIDER_STEPS: u8 = 100; + pub fn main() -> iced::Result { iced::application( "QR Code Generator - Iced", @@ -15,12 +21,16 @@ pub fn main() -> iced::Result { struct QRGenerator { data: String, qr_code: Option, + display_with_fixed_size: bool, + fixed_size_slider_value: u8, theme: Theme, } #[derive(Debug, Clone)] enum Message { DataChanged(String), + SetDisplayWithFixedSize(bool), + FixedSizeSliderChanged(u8), ThemeChanged(Theme), } @@ -38,6 +48,12 @@ impl QRGenerator { self.data = data; } + Message::SetDisplayWithFixedSize(exact_size) => { + self.display_with_fixed_size = exact_size; + } + Message::FixedSizeSliderChanged(value) => { + self.fixed_size_slider_value = value; + } Message::ThemeChanged(theme) => { self.theme = theme; } @@ -61,11 +77,33 @@ impl QRGenerator { .align_y(Center); let content = column![title, input, choose_theme] - .push_maybe( - self.qr_code - .as_ref() - .map(|data| qr_code(data).cell_size(10)), + .push( + toggler(self.display_with_fixed_size) + .on_toggle(Message::SetDisplayWithFixedSize) + .label("Fixed Size"), ) + .push_maybe(self.display_with_fixed_size.then(|| { + slider( + 1..=QR_CODE_EXACT_SIZE_SLIDER_STEPS, + self.fixed_size_slider_value, + Message::FixedSizeSliderChanged, + ) + })) + .push_maybe(self.qr_code.as_ref().map(|data| { + if self.display_with_fixed_size { + // Convert the slider value to a size in pixels. + let qr_code_size_px = (self.fixed_size_slider_value as f32 + / QR_CODE_EXACT_SIZE_SLIDER_STEPS as f32) + * (QR_CODE_EXACT_SIZE_MAX_PX + - QR_CODE_EXACT_SIZE_MIN_PX) + as f32 + + QR_CODE_EXACT_SIZE_MIN_PX as f32; + + qr_code(data).total_size(qr_code_size_px) + } else { + qr_code(data).cell_size(10.0) + } + })) .width(700) .spacing(20) .align_x(Center); diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 21dee6b1..6a8f5ed7 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -34,7 +34,7 @@ use crate::Renderer; use std::cell::RefCell; use thiserror::Error; -const DEFAULT_CELL_SIZE: u16 = 4; +const DEFAULT_CELL_SIZE: f32 = 4.0; const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which @@ -66,7 +66,7 @@ where Theme: Catalog, { data: &'a Data, - cell_size: u16, + cell_size: f32, class: Theme::Class<'a>, } @@ -84,11 +84,17 @@ where } /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: u16) -> Self { + pub fn cell_size(mut self, cell_size: f32) -> Self { self.cell_size = cell_size; self } + /// Sets the size of the entire [`QRCode`]. + pub fn total_size(mut self, total_size: f32) -> Self { + self.cell_size = total_size / (self.data.width + 2 * QUIET_ZONE) as f32; + self + } + /// Sets the style of the [`QRCode`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self @@ -133,8 +139,8 @@ where _renderer: &Renderer, _limits: &layout::Limits, ) -> layout::Node { - let side_length = (self.data.width + 2 * QUIET_ZONE) as f32 - * f32::from(self.cell_size); + let side_length = + (self.data.width + 2 * QUIET_ZONE) as f32 * self.cell_size; layout::Node::new(Size::new(side_length, side_length)) } From 8b34f99b02858a8a0986fd106236f322b35f34a0 Mon Sep 17 00:00:00 2001 From: ibaryshnikov Date: Thu, 26 Sep 2024 17:13:32 +0300 Subject: [PATCH 355/657] added physical_key to KeyReleased event --- core/src/keyboard/event.rs | 3 +++ winit/src/conversion.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 26c45717..0c97d26f 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -36,6 +36,9 @@ pub enum Event { /// The key released. key: Key, + /// The physical key released. + physical_key: key::Physical, + /// The location of the key. location: Location, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 43e1848b..04cbb982 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -262,6 +262,7 @@ pub fn window_event( winit::event::ElementState::Released => { keyboard::Event::KeyReleased { key, + physical_key, modifiers, location, } From 509a0a574a2aa343643dfea63eb154ea46c4ef7f Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Fri, 27 Sep 2024 08:58:33 -0700 Subject: [PATCH 356/657] Don't fill out of viewport text --- core/src/widget/text.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 8b02f8c2..813b4be8 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -334,6 +334,10 @@ pub fn draw( { let bounds = layout.bounds(); + if !bounds.intersects(viewport) { + return; + } + let x = match paragraph.horizontal_alignment() { alignment::Horizontal::Left => bounds.x, alignment::Horizontal::Center => bounds.center_x(), From afecc0f367aff12fa353c8158fac3098ffe40136 Mon Sep 17 00:00:00 2001 From: bbb651 Date: Sun, 29 Sep 2024 16:28:05 +0300 Subject: [PATCH 357/657] Document `File{Dropped,Hovered,HoveredLeft}` as unsupported on wayland Blocked on https://github.com/rust-windowing/winit/issues/1881 --- core/src/window/event.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/window/event.rs b/core/src/window/event.rs index c9532e0d..4e2751ee 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -46,17 +46,29 @@ pub enum Event { /// /// When the user hovers multiple files at once, this event will be emitted /// for each file separately. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Not implemented. FileHovered(PathBuf), /// A file has been dropped into the window. /// /// When the user drops multiple files at once, this event will be emitted /// for each file separately. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Not implemented. FileDropped(PathBuf), /// A file was hovered, but has exited the window. /// /// There will be a single `FilesHoveredLeft` event triggered even if /// multiple files were hovered. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Not implemented. FilesHoveredLeft, } From 7a86900a45116994a4a175c8979550b28ec0dcfb Mon Sep 17 00:00:00 2001 From: bbb651 Date: Sun, 29 Sep 2024 16:37:35 +0300 Subject: [PATCH 358/657] Fix various typos Using https://github.com/crate-ci/typos --- core/src/keyboard/key.rs | 2 +- core/src/keyboard/modifiers.rs | 2 +- core/src/text.rs | 4 ++-- graphics/src/compositor.rs | 2 +- runtime/src/system.rs | 2 +- runtime/src/window.rs | 2 +- widget/src/button.rs | 4 ++-- widget/src/checkbox.rs | 4 ++-- widget/src/combo_box.rs | 4 ++-- widget/src/markdown.rs | 4 ++-- widget/src/text/rich.rs | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index 479d999b..69a91902 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -203,7 +203,7 @@ pub enum Named { Standby, /// The WakeUp key. (`KEYCODE_WAKEUP`) WakeUp, - /// Initate the multi-candidate mode. + /// Initiate the multi-candidate mode. AllCandidates, Alphanumeric, /// Initiate the Code Input mode to allow characters to be entered by diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index edbf6d38..00b31882 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -33,7 +33,7 @@ impl Modifiers { /// This is normally the main modifier to be used for hotkeys. /// /// On macOS, this is equivalent to `Self::LOGO`. - /// Ohterwise, this is equivalent to `Self::CTRL`. + /// Otherwise, this is equivalent to `Self::CTRL`. pub const COMMAND: Self = if cfg!(target_os = "macos") { Self::LOGO } else { diff --git a/core/src/text.rs b/core/src/text.rs index d7b7fee4..a9e3dce5 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -411,13 +411,13 @@ impl<'a, Link, Font> Span<'a, Link, Font> { self } - /// Sets whether the [`Span`] shoud be underlined or not. + /// Sets whether the [`Span`] should be underlined or not. pub fn underline(mut self, underline: bool) -> Self { self.underline = underline; self } - /// Sets whether the [`Span`] shoud be struck through or not. + /// Sets whether the [`Span`] should be struck through or not. pub fn strikethrough(mut self, strikethrough: bool) -> Self { self.strikethrough = strikethrough; self diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 47521eb0..3026bead 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -155,7 +155,7 @@ impl Compositor for () { async fn with_backend( _settings: Settings, _compatible_window: W, - _preffered_backend: Option<&str>, + _preferred_backend: Option<&str>, ) -> Result { Ok(()) } diff --git a/runtime/src/system.rs b/runtime/src/system.rs index b6fb4fdf..8b0ec2d8 100644 --- a/runtime/src/system.rs +++ b/runtime/src/system.rs @@ -8,7 +8,7 @@ pub enum Action { QueryInformation(oneshot::Sender), } -/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). +/// Contains information about the system (e.g. system name, processor, memory, graphics adapter). #[derive(Clone, Debug)] pub struct Information { /// The operating system name diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 27035d7d..382f4518 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -220,7 +220,7 @@ pub fn resize_events() -> Subscription<(Id, Size)> { }) } -/// Subscribes to all [`Event::CloseRequested`] occurences in the running application. +/// Subscribes to all [`Event::CloseRequested`] occurrences in the running application. pub fn close_requests() -> Subscription { event::listen_with(|event, _status, id| { if let crate::core::Event::Window(Event::CloseRequested) = event { diff --git a/widget/src/button.rs b/widget/src/button.rs index 3323c0d3..552298bb 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -477,9 +477,9 @@ pub struct Style { pub background: Option, /// The text [`Color`] of the button. pub text_color: Color, - /// The [`Border`] of the buton. + /// The [`Border`] of the button. pub border: Border, - /// The [`Shadow`] of the butoon. + /// The [`Shadow`] of the button. pub shadow: Shadow, } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 4a3f35ed..4b2f6075 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -487,7 +487,7 @@ pub struct Style { pub background: Background, /// The icon [`Color`] of the checkbox. pub icon_color: Color, - /// The [`Border`] of hte checkbox. + /// The [`Border`] of the checkbox. pub border: Border, /// The text [`Color`] of the checkbox. pub text_color: Option, @@ -600,7 +600,7 @@ pub fn success(theme: &Theme, status: Status) -> Style { } } -/// A danger checkbox; denoting a negaive toggle. +/// A danger checkbox; denoting a negative toggle. pub fn danger(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index fb661ad5..e300f1d0 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -608,8 +608,8 @@ where .. }) = event { - let shift_modifer = modifiers.shift(); - match (named_key, shift_modifer) { + let shift_modifier = modifiers.shift(); + match (named_key, shift_modifier) { (key::Named::Enter, _) => { if let Some(index) = &menu.hovered_option { if let Some(option) = diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 8adc368c..d3f88310 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -1,6 +1,6 @@ //! Markdown widgets can parse and display Markdown. //! -//! You can enable the `highlighter` feature for syntax highligting +//! You can enable the `highlighter` feature for syntax highlighting //! in code blocks. //! //! Only the variants of [`Item`] are currently supported. @@ -72,7 +72,7 @@ pub enum Item { Paragraph(Text), /// A code block. /// - /// You can enable the `highlighter` feature for syntax highligting. + /// You can enable the `highlighter` feature for syntax highlighting. CodeBlock(Text), /// A list. List { diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 921c55a5..3d241375 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -72,7 +72,7 @@ where self } - /// Sets the defualt [`LineHeight`] of the [`Rich`] text. + /// Sets the default [`LineHeight`] of the [`Rich`] text. pub fn line_height(mut self, line_height: impl Into) -> Self { self.line_height = line_height.into(); self From 8028a0ddce5e1313a2b255d9c50924ee78c49980 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Sun, 29 Sep 2024 22:17:30 -0500 Subject: [PATCH 359/657] fix: circular progress no longer skips --- examples/loading_spinners/src/circular.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index bf70e190..9239f01f 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -139,8 +139,8 @@ impl Animation { progress: 0.0, rotation: rotation.wrapping_add( BASE_ROTATION_SPEED.wrapping_add( - (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::MAX) - as u32, + (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) + * u32::MAX as f64) as u32, ), ), last: now, From 8d66b9788864a0df26dc5aeb7f3eca4547dfe4b2 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Tue, 1 Oct 2024 19:30:10 -0700 Subject: [PATCH 360/657] Change lifetime of markdown view IntoIterator Item, as it does not need to live as long as the returned Element. --- widget/src/markdown.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 8adc368c..912ef2f5 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -613,8 +613,8 @@ impl Style { /// } /// } /// ``` -pub fn view<'a, Theme, Renderer>( - items: impl IntoIterator, +pub fn view<'a, 'b, Theme, Renderer>( + items: impl IntoIterator, settings: Settings, style: Style, ) -> Element<'a, Url, Theme, Renderer> From a5c42d4cb171fd408a9273e8ec6183f5091abae7 Mon Sep 17 00:00:00 2001 From: edwloef Date: Mon, 30 Sep 2024 11:02:30 +0200 Subject: [PATCH 361/657] Derive `Default` for `iced_wgpu::geometry::Cache` --- wgpu/src/geometry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index a27cef07..d2ffda53 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -31,7 +31,7 @@ pub enum Geometry { Cached(Cache), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Cache { pub meshes: Option, pub images: Option>, From d40aa6400d7e8692516c2651687790a00deb727e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 2 Oct 2024 15:45:21 +0200 Subject: [PATCH 362/657] Cull widget draw calls in `column` and `row` --- core/src/widget/text.rs | 4 ---- widget/src/column.rs | 19 ++++++++----------- widget/src/row.rs | 19 ++++++++----------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 813b4be8..8b02f8c2 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -334,10 +334,6 @@ pub fn draw( { let bounds = layout.bounds(); - if !bounds.intersects(viewport) { - return; - } - let x = match paragraph.horizontal_alignment() { alignment::Horizontal::Left => bounds.x, alignment::Horizontal::Center => bounds.center_x(), diff --git a/widget/src/column.rs b/widget/src/column.rs index fc4653b9..213f68fc 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -320,24 +320,21 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + for ((child, state), layout) in self .children .iter() .zip(&tree.children) .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) { child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - if self.clip { - &clipped_viewport - } else { - viewport - }, + state, renderer, theme, style, layout, cursor, viewport, ); } } diff --git a/widget/src/row.rs b/widget/src/row.rs index 75d5fb40..9c0fa97e 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -316,24 +316,21 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + for ((child, state), layout) in self .children .iter() .zip(&tree.children) .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) { child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - if self.clip { - &clipped_viewport - } else { - viewport - }, + state, renderer, theme, style, layout, cursor, viewport, ); } } From 32cdc99e928876ef75c6543362665d82cee63f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 2 Oct 2024 16:53:10 +0200 Subject: [PATCH 363/657] Add `modified_key` to `keyboard::Event::KeyReleased` --- core/src/keyboard/event.rs | 3 +++ winit/src/conversion.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 0c97d26f..6e483f5b 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -36,6 +36,9 @@ pub enum Event { /// The key released. key: Key, + /// The key released with all keyboard modifiers applied, except Ctrl. + modified_key: Key, + /// The physical key released. physical_key: key::Physical, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 04cbb982..5d0f8348 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -262,6 +262,7 @@ pub fn window_event( winit::event::ElementState::Released => { keyboard::Event::KeyReleased { key, + modified_key, physical_key, modifiers, location, From a1e2bd22ec91cce608adc26d6347baf6c054131c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 2 Oct 2024 17:08:53 +0200 Subject: [PATCH 364/657] Simplify total size logic in `qr_code` example --- examples/qr_code/src/main.rs | 80 +++++++++++++++++------------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 638f31c0..2c892e3f 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -3,9 +3,7 @@ use iced::widget::{ }; use iced::{Center, Element, Theme}; -const QR_CODE_EXACT_SIZE_MIN_PX: u32 = 200; -const QR_CODE_EXACT_SIZE_MAX_PX: u32 = 400; -const QR_CODE_EXACT_SIZE_SLIDER_STEPS: u8 = 100; +use std::ops::RangeInclusive; pub fn main() -> iced::Result { iced::application( @@ -21,20 +19,21 @@ pub fn main() -> iced::Result { struct QRGenerator { data: String, qr_code: Option, - display_with_fixed_size: bool, - fixed_size_slider_value: u8, + total_size: Option, theme: Theme, } #[derive(Debug, Clone)] enum Message { DataChanged(String), - SetDisplayWithFixedSize(bool), - FixedSizeSliderChanged(u8), + ToggleTotalSize(bool), + TotalSizeChanged(f32), ThemeChanged(Theme), } impl QRGenerator { + const SIZE_RANGE: RangeInclusive = 200.0..=400.0; + fn update(&mut self, message: Message) { match message { Message::DataChanged(mut data) => { @@ -48,11 +47,15 @@ impl QRGenerator { self.data = data; } - Message::SetDisplayWithFixedSize(exact_size) => { - self.display_with_fixed_size = exact_size; + Message::ToggleTotalSize(enabled) => { + self.total_size = enabled.then_some( + Self::SIZE_RANGE.start() + + (Self::SIZE_RANGE.end() - Self::SIZE_RANGE.start()) + / 2.0, + ); } - Message::FixedSizeSliderChanged(value) => { - self.fixed_size_slider_value = value; + Message::TotalSizeChanged(total_size) => { + self.total_size = Some(total_size); } Message::ThemeChanged(theme) => { self.theme = theme; @@ -69,6 +72,10 @@ impl QRGenerator { .size(30) .padding(15); + let toggle_total_size = toggler(self.total_size.is_some()) + .on_toggle(Message::ToggleTotalSize) + .label("Limit Total Size"); + let choose_theme = row![ text("Theme:"), pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,) @@ -76,37 +83,26 @@ impl QRGenerator { .spacing(10) .align_y(Center); - let content = column![title, input, choose_theme] - .push( - toggler(self.display_with_fixed_size) - .on_toggle(Message::SetDisplayWithFixedSize) - .label("Fixed Size"), - ) - .push_maybe(self.display_with_fixed_size.then(|| { - slider( - 1..=QR_CODE_EXACT_SIZE_SLIDER_STEPS, - self.fixed_size_slider_value, - Message::FixedSizeSliderChanged, - ) - })) - .push_maybe(self.qr_code.as_ref().map(|data| { - if self.display_with_fixed_size { - // Convert the slider value to a size in pixels. - let qr_code_size_px = (self.fixed_size_slider_value as f32 - / QR_CODE_EXACT_SIZE_SLIDER_STEPS as f32) - * (QR_CODE_EXACT_SIZE_MAX_PX - - QR_CODE_EXACT_SIZE_MIN_PX) - as f32 - + QR_CODE_EXACT_SIZE_MIN_PX as f32; - - qr_code(data).total_size(qr_code_size_px) - } else { - qr_code(data).cell_size(10.0) - } - })) - .width(700) - .spacing(20) - .align_x(Center); + let content = column![ + title, + input, + row![toggle_total_size, choose_theme] + .spacing(20) + .align_y(Center) + ] + .push_maybe(self.total_size.map(|total_size| { + slider(Self::SIZE_RANGE, total_size, Message::TotalSizeChanged) + })) + .push_maybe(self.qr_code.as_ref().map(|data| { + if let Some(total_size) = self.total_size { + qr_code(data).total_size(total_size) + } else { + qr_code(data).cell_size(10.0) + } + })) + .width(700) + .spacing(20) + .align_x(Center); center(content).padding(20).into() } From b02ec8b6b2f2bea68256a91e2a8a8c17198d4ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 2 Oct 2024 17:12:12 +0200 Subject: [PATCH 365/657] Make `cell_size` and `total_size` generic over `Pixels` in `qr_code` --- widget/src/qr_code.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 6a8f5ed7..d1834465 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -26,8 +26,8 @@ use crate::core::mouse; use crate::core::renderer::{self, Renderer as _}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector, - Widget, + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, + Vector, Widget, }; use crate::Renderer; @@ -84,14 +84,16 @@ where } /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: f32) -> Self { - self.cell_size = cell_size; + pub fn cell_size(mut self, cell_size: impl Into) -> Self { + self.cell_size = cell_size.into().0; self } /// Sets the size of the entire [`QRCode`]. - pub fn total_size(mut self, total_size: f32) -> Self { - self.cell_size = total_size / (self.data.width + 2 * QUIET_ZONE) as f32; + pub fn total_size(mut self, total_size: impl Into) -> Self { + self.cell_size = + total_size.into().0 / (self.data.width + 2 * QUIET_ZONE) as f32; + self } From 89c665481077dbc64645caf1161dcd15e7e2386f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 4 Oct 2024 16:48:46 +0200 Subject: [PATCH 366/657] Fix `Task::chain` when chained task is `Task::none` --- 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 3f97d134..4554c74b 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -117,7 +117,7 @@ impl Task { match self.0 { None => task, Some(first) => match task.0 { - None => Task::none(), + None => Task(Some(first)), Some(second) => Task(Some(boxed_stream(first.chain(second)))), }, } From fca5d8038adcae4587cc553a15f89bc632f6ee95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 4 Oct 2024 16:58:05 +0200 Subject: [PATCH 367/657] Implement `Overlay::operate` for `responsive::Overlay` --- widget/src/lazy/responsive.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index dbf281f3..2b92c6dc 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -453,4 +453,15 @@ where }) .unwrap_or_default() } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.operate(layout, renderer, operation); + }); + } } From d057b16153c6772b80a296a8c17fb67da51eb07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 4 Oct 2024 17:05:32 +0200 Subject: [PATCH 368/657] Fix `responsive`, `component`, and `lazy` always returning an `overlay` --- widget/src/lazy.rs | 55 +++++++++++++++---------- widget/src/lazy/component.rs | 76 ++++++++++++++++++----------------- widget/src/lazy/responsive.rs | 6 ++- 3 files changed, 78 insertions(+), 59 deletions(-) diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 221f9de3..6642c986 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -270,29 +270,40 @@ where renderer: &Renderer, translation: Vector, ) -> Option> { - let overlay = Overlay(Some( - InnerBuilder { - cell: self.element.borrow().as_ref().unwrap().clone(), - element: self - .element - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .take() - .unwrap(), - tree: &mut tree.children[0], - overlay_builder: |element, tree| { - element - .as_widget_mut() - .overlay(tree, layout, renderer, translation) - .map(|overlay| RefCell::new(Nested::new(overlay))) - }, - } - .build(), - )); + let overlay = InnerBuilder { + cell: self.element.borrow().as_ref().unwrap().clone(), + element: self + .element + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .take() + .unwrap(), + tree: &mut tree.children[0], + overlay_builder: |element, tree| { + element + .as_widget_mut() + .overlay(tree, layout, renderer, translation) + .map(|overlay| RefCell::new(Nested::new(overlay))) + }, + } + .build(); - Some(overlay::Element::new(Box::new(overlay))) + #[allow(clippy::redundant_closure_for_method_calls)] + if overlay.with_overlay(|overlay| overlay.is_some()) { + Some(overlay::Element::new(Box::new(Overlay(Some(overlay))))) + } else { + let heads = overlay.into_heads(); + + // - You may not like it, but this is what peak performance looks like + // - TODO: Get rid of ouroboros, for good + // - What?! + *self.element.borrow().as_ref().unwrap().borrow_mut() = + Some(heads.element); + + None + } } } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 2bdfa2c0..c7bc1264 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -446,44 +446,48 @@ where ) -> Option> { self.rebuild_element_if_necessary(); - let tree = tree - .state - .downcast_mut::>>>() - .borrow_mut() - .take() - .unwrap(); + let state = tree.state.downcast_mut::>>>(); + let tree = state.borrow_mut().take().unwrap(); - let overlay = Overlay(Some( - InnerBuilder { - instance: self, - tree, - types: PhantomData, - overlay_builder: |instance, tree| { - instance.state.get_mut().as_mut().unwrap().with_element_mut( - move |element| { - element - .as_mut() - .unwrap() - .as_widget_mut() - .overlay( - &mut tree.children[0], - layout, - renderer, - translation, - ) - .map(|overlay| { - RefCell::new(Nested::new(overlay)) - }) - }, - ) - }, - } - .build(), - )); + let overlay = InnerBuilder { + instance: self, + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance.state.get_mut().as_mut().unwrap().with_element_mut( + move |element| { + element + .as_mut() + .unwrap() + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout, + renderer, + translation, + ) + .map(|overlay| RefCell::new(Nested::new(overlay))) + }, + ) + }, + } + .build(); - Some(overlay::Element::new(Box::new(OverlayInstance { - overlay: Some(overlay), - }))) + #[allow(clippy::redundant_closure_for_method_calls)] + if overlay.with_overlay(|overlay| overlay.is_some()) { + Some(overlay::Element::new(Box::new(OverlayInstance { + overlay: Some(Overlay(Some(overlay))), // Beautiful, I know + }))) + } else { + let heads = overlay.into_heads(); + + // - You may not like it, but this is what peak performance looks like + // - TODO: Get rid of ouroboros, for good + // - What?! + *state.borrow_mut() = Some(heads.tree); + + None + } } } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 2b92c6dc..a7a99f56 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -320,7 +320,11 @@ where } .build(); - Some(overlay::Element::new(Box::new(overlay))) + if overlay.with_overlay(|(overlay, _layout)| overlay.is_some()) { + Some(overlay::Element::new(Box::new(overlay))) + } else { + None + } } } From c217500a5abd1fbfc1e598fb98dadd3ee85d8a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 4 Oct 2024 17:33:38 +0200 Subject: [PATCH 369/657] Fix `mouse::Cursor` fighting in `stack` widget --- widget/src/stack.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 9ccaa274..6a44c328 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -215,9 +215,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { - let is_over_scroll = - matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. })) - && cursor.is_over(layout.bounds()); + let is_over = cursor.is_over(layout.bounds()); self.children .iter_mut() @@ -236,7 +234,7 @@ where viewport, ); - if is_over_scroll && cursor != mouse::Cursor::Unavailable { + if is_over && cursor != mouse::Cursor::Unavailable { let interaction = child.as_widget().mouse_interaction( state, layout, cursor, viewport, renderer, ); From 13c649881edfda9ab0215c7353a5236e07ef749d Mon Sep 17 00:00:00 2001 From: bbb651 Date: Fri, 4 Oct 2024 21:14:06 +0300 Subject: [PATCH 370/657] Add `window::Settings::maximized` Corresponds to `winit::window::WindowAttributes::with_maximized` --- core/src/window/settings.rs | 4 ++++ winit/src/conversion.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index fbbf86ab..13822e82 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -34,6 +34,9 @@ pub struct Settings { /// The initial logical dimensions of the window. pub size: Size, + /// Whether the window should start maximized. + pub maximized: bool, + /// The initial position of the window. pub position: Position, @@ -79,6 +82,7 @@ impl Default for Settings { fn default() -> Self { Self { size: Size::new(1024.0, 768.0), + maximized: false, position: Position::default(), min_size: None, max_size: None, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 5d0f8348..e0b0569b 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -23,6 +23,7 @@ pub fn window_attributes( width: settings.size.width, height: settings.size.height, }) + .with_maximized(settings.maximized) .with_resizable(settings.resizable) .with_enabled_buttons(if settings.resizable { winit::window::WindowButtons::all() From dd08f98f0ebb6fb59801bfa030a56267e45a509b Mon Sep 17 00:00:00 2001 From: bbb651 Date: Fri, 4 Oct 2024 21:20:43 +0300 Subject: [PATCH 371/657] Add `window::Settings::fullscreen` Corresponds to `winit::window::WindowAttributes::with_fullscreen`. Currently only allows to set `Fullscreen::Borderless(None)` meaning borderless on the current monitor, exclusive fullscreen does not make sense for a GUI and iced does not expose monitors yet. --- core/src/window/settings.rs | 4 ++++ winit/src/conversion.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index 13822e82..e87fcc83 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -37,6 +37,9 @@ pub struct Settings { /// Whether the window should start maximized. pub maximized: bool, + /// Whether the window should start fullscreen. + pub fullscreen: bool, + /// The initial position of the window. pub position: Position, @@ -83,6 +86,7 @@ impl Default for Settings { Self { size: Size::new(1024.0, 768.0), maximized: false, + fullscreen: false, position: Position::default(), min_size: None, max_size: None, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index e0b0569b..e454c208 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -24,6 +24,11 @@ pub fn window_attributes( height: settings.size.height, }) .with_maximized(settings.maximized) + .with_fullscreen( + settings + .fullscreen + .then_some(winit::window::Fullscreen::Borderless(None)), + ) .with_resizable(settings.resizable) .with_enabled_buttons(if settings.resizable { winit::window::WindowButtons::all() From f912d26d64c80fe767e97a0c79416d7398f04488 Mon Sep 17 00:00:00 2001 From: BradySimon Date: Sat, 12 Oct 2024 19:55:41 -0400 Subject: [PATCH 372/657] Add `PartialEq` derives for widget styles --- core/src/widget/text.rs | 2 +- widget/src/checkbox.rs | 2 +- widget/src/container.rs | 2 +- widget/src/overlay/menu.rs | 2 +- widget/src/pick_list.rs | 2 +- widget/src/progress_bar.rs | 2 +- widget/src/radio.rs | 2 +- widget/src/rule.rs | 4 ++-- widget/src/scrollable.rs | 6 +++--- widget/src/slider.rs | 8 ++++---- widget/src/text_editor.rs | 2 +- widget/src/text_input.rs | 2 +- widget/src/toggler.rs | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 8b02f8c2..b34c5632 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -389,7 +389,7 @@ where } /// The appearance of some text. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Style { /// The [`Color`] of the text. /// diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 4b2f6075..819f0d9d 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -481,7 +481,7 @@ pub enum Status { } /// The style of a checkbox. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the checkbox. pub background: Background, diff --git a/widget/src/container.rs b/widget/src/container.rs index b256540c..f4993ac9 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -572,7 +572,7 @@ pub fn visible_bounds(id: Id) -> Task> { } /// The appearance of a container. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Style { /// The text [`Color`] of the container. pub text_color: Option, diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index f05ae40a..b641e8f5 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -562,7 +562,7 @@ where } /// The appearance of a [`Menu`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the menu. pub background: Background, diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index ff54fe8a..4f1e9da9 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -828,7 +828,7 @@ pub enum Status { } /// The appearance of a pick list. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The text [`Color`] of the pick list. pub text_color: Color, diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 8c665c8c..9d2b30f4 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -208,7 +208,7 @@ where } /// The appearance of a progress bar. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the progress bar. pub background: Background, diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 300318fd..d2a3bd6a 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -471,7 +471,7 @@ pub enum Status { } /// The appearance of a radio button. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the radio button. pub background: Background, diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 92199ca9..24577683 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -187,7 +187,7 @@ where } /// The appearance of a rule. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The color of the rule. pub color: Color, @@ -200,7 +200,7 @@ pub struct Style { } /// The fill mode of a rule. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum FillMode { /// Fill the whole length of the container. Full, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 6d7f251e..528d63c1 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1856,7 +1856,7 @@ pub enum Status { } /// The appearance of a scrollable. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`container::Style`] of a scrollable. pub container: container::Style, @@ -1869,7 +1869,7 @@ pub struct Style { } /// The appearance of the scrollbar of a scrollable. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Rail { /// The [`Background`] of a scrollbar. pub background: Option, @@ -1880,7 +1880,7 @@ pub struct Rail { } /// The appearance of the scroller of a scrollable. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Scroller { /// The [`Color`] of the scroller. pub color: Color, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 9477958d..31aa0e0c 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -562,7 +562,7 @@ pub enum Status { } /// The appearance of a slider. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The colors of the rail of the slider. pub rail: Rail, @@ -582,7 +582,7 @@ impl Style { } /// The appearance of a slider rail -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Rail { /// The backgrounds of the rail of the slider. pub backgrounds: (Background, Background), @@ -593,7 +593,7 @@ pub struct Rail { } /// The appearance of the handle of a slider. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Handle { /// The shape of the handle. pub shape: HandleShape, @@ -606,7 +606,7 @@ pub struct Handle { } /// The shape of the handle of a slider. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum HandleShape { /// A circular handle. Circle { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a9322474..30575559 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1226,7 +1226,7 @@ pub enum Status { } /// The appearance of a text input. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the text input. pub background: Background, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 5bbf76f5..ff413779 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1541,7 +1541,7 @@ pub enum Status { } /// The appearance of a text input. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the text input. pub background: Background, diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 3b412081..fdd2e68c 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -489,7 +489,7 @@ pub enum Status { } /// The appearance of a toggler. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The background [`Color`] of the toggler. pub background: Color, From da1331169cdaa27d4e2c080bc0f2aa6457f0537c Mon Sep 17 00:00:00 2001 From: Leo Ring Date: Tue, 8 Oct 2024 01:47:01 +0100 Subject: [PATCH 373/657] Fix `Binding::Delete` not triggering in `text_editor` --- widget/src/text_editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a9322474..3676d02f 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1045,7 +1045,9 @@ impl Binding { keyboard::Key::Named(key::Named::Backspace) => { Some(Self::Backspace) } - keyboard::Key::Named(key::Named::Delete) if text.is_none() => { + keyboard::Key::Named(key::Named::Delete) + if text.is_none() || text.as_deref() == Some("\u{7f}") => + { Some(Self::Delete) } keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), From 6c5799e759c5c8384d53b7e73e5a0b580008bf3f Mon Sep 17 00:00:00 2001 From: l-const Date: Mon, 14 Oct 2024 23:01:07 +0300 Subject: [PATCH 374/657] Introduce consecutive click distance like other toolkits such as gtk,qt, imgui. --- core/src/mouse/click.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 07a4db5a..0a373878 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -82,7 +82,7 @@ impl Click { None }; - self.position == new_position + self.position.distance(new_position) < 6.0 && duration .map(|duration| duration.as_millis() <= 300) .unwrap_or(false) From 4e0a63091cf1b5dbff0271200eac49d1b65e77e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 17 Oct 2024 06:14:30 +0200 Subject: [PATCH 375/657] Fix new elided lifetime lint in the `beta` toolchain --- widget/src/lazy.rs | 2 +- widget/src/pane_grid.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 6642c986..232f254c 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -269,7 +269,7 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option> { + ) -> Option> { let overlay = InnerBuilder { cell: self.element.borrow().as_ref().unwrap().clone(), element: self diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 9d4dda25..e6fda660 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -881,7 +881,7 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option> { + ) -> Option> { let children = self .contents .iter_mut() From ab2adb11be28a3e44aa77daf41bbf7d86233d4ea Mon Sep 17 00:00:00 2001 From: Michelle Granat <139873465+MichelleGranat@users.noreply.github.com> Date: Thu, 17 Oct 2024 07:29:31 +0300 Subject: [PATCH 376/657] Update button Catalog and Style documentation (#2590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update button Catalog and Style documentation * Clarified button documentation * fix code typo * Run `cargo fmt` * Fixed docs to pass tests --------- Co-authored-by: Héctor Ramón Jiménez --- widget/src/button.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/widget/src/button.rs b/widget/src/button.rs index 552298bb..a3394a01 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -471,6 +471,9 @@ pub enum Status { } /// The style of a button. +/// +/// If not specified with [`Button::style`] +/// the theme will provide the style. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the button. @@ -505,6 +508,54 @@ impl Default for Style { } /// The theme catalog of a [`Button`]. +/// +/// All themes that can be used with [`Button`] +/// must implement this trait. +/// +/// # Example +/// ```no_run +/// # use iced_widget::core::{Color, Background}; +/// # use iced_widget::button::{Catalog, Status, Style}; +/// # struct MyTheme; +/// #[derive(Debug, Default)] +/// pub enum ButtonClass { +/// #[default] +/// Primary, +/// Secondary, +/// Danger +/// } +/// +/// impl Catalog for MyTheme { +/// type Class<'a> = ButtonClass; +/// +/// fn default<'a>() -> Self::Class<'a> { +/// ButtonClass::default() +/// } +/// +/// +/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { +/// let mut style = Style::default(); +/// +/// match class { +/// ButtonClass::Primary => { +/// style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921))); +/// }, +/// ButtonClass::Secondary => { +/// style.background = Some(Background::Color(Color::WHITE)); +/// }, +/// ButtonClass::Danger => { +/// style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502))); +/// }, +/// } +/// +/// style +/// } +/// } +/// ``` +/// +/// Although, in order to use [`Button::style`] +/// with `MyTheme`, [`Catalog::Class`] must implement +/// `From>`. pub trait Catalog { /// The item class of the [`Catalog`]. type Class<'a>; From ad34f03df4abb2c430bf6a4f5ac030d12d05863e Mon Sep 17 00:00:00 2001 From: Kevin Day Date: Sat, 19 Oct 2024 16:12:38 +1000 Subject: [PATCH 377/657] Modified clock example to make the clock more readable. Added numbers on the clock face and took the portion of the hour passed into consideration for the hour hand. It now looks like a reasonable clock. --- examples/clock/src/main.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index ef3064c7..a7e86ff1 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,4 +1,4 @@ -use iced::alignment; +use iced::{alignment, Radians}; use iced::mouse; use iced::time; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; @@ -117,9 +117,12 @@ impl canvas::Program for Clock { }; frame.translate(Vector::new(center.x, center.y)); + let minutes_portion = Radians::from(hand_rotation(self.now.minute(), 60)) / 12.0; + let hour_hand_angle = Radians::from(hand_rotation(self.now.hour(), 12)) + minutes_portion; + frame.with_save(|frame| { - frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.rotate(hour_hand_angle); frame.stroke(&short_hand, wide_stroke()); }); @@ -155,10 +158,31 @@ impl canvas::Program for Clock { ..canvas::Text::default() }); }); + + // Draw clock numbers + for i in 1..=12 { + let distance_out = radius * 1.05; + let angle = + Radians::from(hand_rotation(i, 12)) - Radians::from(Degrees(90.0)); + let x = distance_out * angle.0.cos(); + let y = distance_out * angle.0.sin(); + + frame.fill_text(canvas::Text { + content: format!("{}", i), + size: (radius / 15.0).into(), + position: Point::new(x * 0.85, y * 0.85), + color: palette.secondary.strong.text, + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + font: Font::MONOSPACE, + ..canvas::Text::default() + }); + } }); vec![clock] } + } fn hand_rotation(n: u32, total: u32) -> Degrees { From f4d03870dd1365e8931c6f8e203eda940de735d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 03:01:24 +0200 Subject: [PATCH 378/657] Dismiss `large-enum-variant` lint --- examples/changelog/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/changelog/Cargo.toml b/examples/changelog/Cargo.toml index eb942235..eeb7b526 100644 --- a/examples/changelog/Cargo.toml +++ b/examples/changelog/Cargo.toml @@ -5,6 +5,9 @@ authors = ["Héctor Ramón Jiménez "] edition = "2021" publish = false +[lints.clippy] +large_enum_variant = "allow" + [dependencies] iced.workspace = true iced.features = ["tokio", "markdown", "highlighter", "debug"] From 415fd4f643d385dad39bd0ee5a47de384643dcf3 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Fri, 4 Oct 2024 11:27:02 -0700 Subject: [PATCH 379/657] Use BTreeMap for Ord iteration of panes This ensures continuity in how panes are iterated on when building widget state --- widget/src/pane_grid/state.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index c20c3b9c..f5d2bb02 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -6,7 +6,7 @@ use crate::pane_grid::{ Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, }; -use rustc_hash::FxHashMap; +use std::collections::BTreeMap; /// The state of a [`PaneGrid`]. /// @@ -25,7 +25,7 @@ pub struct State { /// The panes of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid - pub panes: FxHashMap, + pub panes: BTreeMap, /// The internal state of the [`PaneGrid`]. /// @@ -52,7 +52,7 @@ impl State { /// Creates a new [`State`] with the given [`Configuration`]. pub fn with_configuration(config: impl Into>) -> Self { - let mut panes = FxHashMap::default(); + let mut panes = BTreeMap::default(); let internal = Internal::from_configuration(&mut panes, config.into(), 0); @@ -353,7 +353,7 @@ impl Internal { /// /// [`PaneGrid`]: super::PaneGrid pub fn from_configuration( - panes: &mut FxHashMap, + panes: &mut BTreeMap, content: Configuration, next_id: usize, ) -> Self { From 9ac3318357d636cde22ae34f7b2cdeddd3f55cdb Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Fri, 4 Oct 2024 11:31:14 -0700 Subject: [PATCH 380/657] Retain widget state against incoming panes We can associate each state with a `Pane` and compare that against the new panes to remove states w/ respective panes which no longer exist. Because we always increment `Pane`, new states are always added to the end, so this retain + add new state approach will ensure continuity when panes are added & removed --- widget/src/pane_grid.rs | 246 ++++++++++++++++++---------------- widget/src/pane_grid/state.rs | 14 +- 2 files changed, 135 insertions(+), 125 deletions(-) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index e6fda660..2644986f 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -92,6 +92,8 @@ use crate::core::{ Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; +use std::borrow::Cow; + const DRAG_DEADBAND_DISTANCE: f32 = 10.0; const THICKNESS_RATIO: f32 = 25.0; @@ -157,7 +159,10 @@ pub struct PaneGrid< Theme: Catalog, Renderer: core::Renderer, { - contents: Contents<'a, Content<'a, Message, Theme, Renderer>>, + internal: &'a state::Internal, + panes: Vec, + contents: Vec>, + maximized: Option, width: Length, height: Length, spacing: f32, @@ -180,30 +185,21 @@ where state: &'a State, view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>, ) -> Self { - let contents = if let Some((pane, pane_state)) = - state.maximized.and_then(|pane| { - state.panes.get(&pane).map(|pane_state| (pane, pane_state)) - }) { - Contents::Maximized( - pane, - view(pane, pane_state, true), - Node::Pane(pane), - ) - } else { - Contents::All( - state - .panes - .iter() - .map(|(pane, pane_state)| { - (*pane, view(*pane, pane_state, false)) - }) - .collect(), - &state.internal, - ) - }; + let panes = state.panes.keys().copied().collect(); + let contents = state + .panes + .iter() + .map(|(pane, pane_state)| match &state.maximized { + Some(p) if pane == p => view(*pane, pane_state, true), + _ => view(*pane, pane_state, false), + }) + .collect(); Self { + internal: &state.internal, + panes, contents, + maximized: state.maximized, width: Length::Fill, height: Length::Fill, spacing: 0.0, @@ -248,7 +244,9 @@ where where F: 'a + Fn(DragEvent) -> Message, { - self.on_drag = Some(Box::new(f)); + if self.maximized.is_none() { + self.on_drag = Some(Box::new(f)); + } self } @@ -265,7 +263,9 @@ where where F: 'a + Fn(ResizeEvent) -> Message, { - self.on_resize = Some((leeway.into().0, Box::new(f))); + if self.maximized.is_none() { + self.on_resize = Some((leeway.into().0, Box::new(f))); + } self } @@ -291,10 +291,17 @@ where } fn drag_enabled(&self) -> bool { - (!self.contents.is_maximized()) + (self.maximized.is_none()) .then(|| self.on_drag.is_some()) .unwrap_or_default() } + + fn node(&self) -> Cow<'_, Node> { + match self.maximized { + Some(pane) => Cow::Owned(Node::Pane(pane)), + None => Cow::Borrowed(&self.internal.layout), + } + } } impl<'a, Message, Theme, Renderer> Widget @@ -304,33 +311,48 @@ where Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(state::Action::Idle) + tree::State::new(state::Widget::default()) } fn children(&self) -> Vec { - self.contents - .iter() - .map(|(_, content)| content.state()) - .collect() + self.contents.iter().map(Content::state).collect() } fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), - Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], - |state, content| content.diff(state), - |content| content.state(), - ), - } + let state::Widget { panes, .. } = tree.state.downcast_ref(); + + // `Pane` always increments and is iterated by Ord so new + // states are always added at the end. We can simply remove + // states which no longer exist and `diff_children` will + // diff the remaining values in the correct order and + // add new states at the end + + let mut i = 0; + let mut j = 0; + tree.children.retain(|_| { + let retain = self.panes.get(i) == panes.get(j); + + if retain { + i += 1; + } + j += 1; + + retain + }); + + tree.diff_children_custom( + &self.contents, + |state, content| content.diff(state), + Content::state, + ); + + let state::Widget { panes, .. } = tree.state.downcast_mut(); + + panes.clone_from(&self.panes); } fn size(&self) -> Size { @@ -347,14 +369,19 @@ where limits: &layout::Limits, ) -> layout::Node { let size = limits.resolve(self.width, self.height, Size::ZERO); - let node = self.contents.layout(); - let regions = node.pane_regions(self.spacing, size); + let regions = self.node().pane_regions(self.spacing, size); let children = self - .contents + .panes .iter() + .copied() + .zip(&self.contents) .zip(tree.children.iter_mut()) .filter_map(|((pane, content), tree)| { + if self.maximized.is_some() && Some(pane) != self.maximized { + return Some(layout::Node::new(Size::ZERO)); + } + let region = regions.get(&pane)?; let size = Size::new(region.width, region.height); @@ -379,11 +406,16 @@ where operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { - self.contents + self.panes .iter() + .copied() + .zip(&self.contents) .zip(&mut tree.children) .zip(layout.children()) - .for_each(|(((_pane, content), state), layout)| { + .filter(|(((pane, _), _), _)| { + self.maximized.map_or(true, |maximized| *pane == maximized) + }) + .for_each(|(((_, content), state), layout)| { content.operate(state, layout, renderer, operation); }); }); @@ -402,8 +434,8 @@ where ) -> event::Status { let mut event_status = event::Status::Ignored; - let action = tree.state.downcast_mut::(); - let node = self.contents.layout(); + let state::Widget { action, .. } = tree.state.downcast_mut(); + let node = self.node(); let on_drag = if self.drag_enabled() { &self.on_drag @@ -448,7 +480,10 @@ where layout, cursor_position, shell, - self.contents.iter(), + self.panes + .iter() + .copied() + .zip(&self.contents), &self.on_click, on_drag, ); @@ -460,7 +495,7 @@ where layout, cursor_position, shell, - self.contents.iter(), + self.panes.iter().copied().zip(&self.contents), &self.on_click, on_drag, ); @@ -486,8 +521,10 @@ where } } else { let dropped_region = self - .contents + .panes .iter() + .copied() + .zip(&self.contents) .zip(layout.children()) .find_map(|(target, layout)| { layout_region( @@ -572,10 +609,15 @@ where let picked_pane = action.picked_pane().map(|(pane, _)| pane); - self.contents - .iter_mut() + self.panes + .iter() + .copied() + .zip(&mut self.contents) .zip(&mut tree.children) .zip(layout.children()) + .filter(|(((pane, _), _), _)| { + self.maximized.map_or(true, |maximized| *pane == maximized) + }) .map(|(((pane, content), tree), layout)| { let is_picked = picked_pane == Some(pane); @@ -602,14 +644,14 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let action = tree.state.downcast_ref::(); + let state::Widget { action, .. } = tree.state.downcast_ref(); if action.picked_pane().is_some() { return mouse::Interaction::Grabbing; } let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); - let node = self.contents.layout(); + let node = self.node(); let resize_axis = action.picked_split().map(|(_, axis)| axis).or_else(|| { @@ -641,11 +683,16 @@ where }; } - self.contents + self.panes .iter() + .copied() + .zip(&self.contents) .zip(&tree.children) .zip(layout.children()) - .map(|(((_pane, content), tree), layout)| { + .filter(|(((pane, _), _), _)| { + self.maximized.map_or(true, |maximized| *pane == maximized) + }) + .map(|(((_, content), tree), layout)| { content.mouse_interaction( tree, layout, @@ -669,16 +716,11 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let action = tree.state.downcast_ref::(); - let node = self.contents.layout(); + let state::Widget { action, .. } = + tree.state.downcast_ref::(); + let node = self.node(); let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); - let contents = self - .contents - .iter() - .zip(&tree.children) - .map(|((pane, content), tree)| (pane, (content, tree))); - let picked_pane = action.picked_pane().filter(|(_, origin)| { cursor .position() @@ -747,8 +789,16 @@ where let style = Catalog::style(theme, &self.class); - for ((id, (content, tree)), pane_layout) in - contents.zip(layout.children()) + for (((id, content), tree), pane_layout) in self + .panes + .iter() + .copied() + .zip(&self.contents) + .zip(&tree.children) + .zip(layout.children()) + .filter(|(((pane, _), _), _)| { + self.maximized.map_or(true, |maximized| maximized == *pane) + }) { match picked_pane { Some((dragging, origin)) if id == dragging => { @@ -883,11 +933,17 @@ where translation: Vector, ) -> Option> { let children = self - .contents - .iter_mut() + .panes + .iter() + .copied() + .zip(&mut self.contents) .zip(&mut tree.children) .zip(layout.children()) - .filter_map(|(((_, content), state), layout)| { + .filter_map(|(((pane, content), state), layout)| { + if self.maximized.is_some() && Some(pane) != self.maximized { + return None; + } + content.overlay(state, layout, renderer, translation) }) .collect::>(); @@ -1136,52 +1192,6 @@ fn hovered_split<'a>( }) } -/// The visible contents of the [`PaneGrid`] -#[derive(Debug)] -pub enum Contents<'a, T> { - /// All panes are visible - All(Vec<(Pane, T)>, &'a state::Internal), - /// A maximized pane is visible - Maximized(Pane, T, Node), -} - -impl<'a, T> Contents<'a, T> { - /// Returns the layout [`Node`] of the [`Contents`] - pub fn layout(&self) -> &Node { - match self { - Contents::All(_, state) => state.layout(), - Contents::Maximized(_, _, layout) => layout, - } - } - - /// Returns an iterator over the values of the [`Contents`] - pub fn iter(&self) -> Box + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn iter_mut(&mut self) -> Box + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter_mut().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn is_maximized(&self) -> bool { - matches!(self, Self::Maximized(..)) - } -} - /// The appearance of a [`PaneGrid`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index f5d2bb02..e1934930 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -343,7 +343,7 @@ impl State { /// [`PaneGrid`]: super::PaneGrid #[derive(Debug, Clone)] pub struct Internal { - layout: Node, + pub(super) layout: Node, last_id: usize, } @@ -397,11 +397,12 @@ impl Internal { /// The current action of a [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum Action { /// The [`PaneGrid`] is idle. /// /// [`PaneGrid`]: super::PaneGrid + #[default] Idle, /// A [`Pane`] in the [`PaneGrid`] is being dragged. /// @@ -441,9 +442,8 @@ impl Action { } } -impl Internal { - /// The layout [`Node`] of the [`Internal`] state - pub fn layout(&self) -> &Node { - &self.layout - } +#[derive(Default)] +pub(super) struct Widget { + pub action: Action, + pub panes: Vec, } From 5ebd8ac83f6c173bd24de146bf582f049663a330 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Fri, 4 Oct 2024 11:34:14 -0700 Subject: [PATCH 381/657] Keep `Pane` associated to state / layout after swap State continuity is dependent on keeping a node associated to it's original `Pane` id. When splitting -> swapping nodes, we need to assign it back to the original `Pane` to enforce continuity. --- widget/src/pane_grid/state.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index e1934930..b7aef67d 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -228,8 +228,15 @@ impl State { ) { if let Some((state, _)) = self.close(pane) { if let Some((new_pane, _)) = self.split(axis, target, state) { + // Ensure new node corresponds to original `Pane` for state continuity + self.swap(pane, new_pane); + let _ = self + .panes + .remove(&new_pane) + .and_then(|state| self.panes.insert(pane, state)); + if swap { - self.swap(target, new_pane); + self.swap(target, pane); } } } @@ -262,7 +269,16 @@ impl State { swap: bool, ) { if let Some((state, _)) = self.close(pane) { - let _ = self.split_node(axis, None, state, swap); + if let Some((new_pane, _)) = + self.split_node(axis, None, state, swap) + { + // Ensure new node corresponds to original `Pane` for state continuity + self.swap(pane, new_pane); + let _ = self + .panes + .remove(&new_pane) + .and_then(|state| self.panes.insert(pane, state)); + } } } From 659669dd5810d470d51f528495cab2f676eb58ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 24 Oct 2024 13:47:28 +0200 Subject: [PATCH 382/657] Remove duplicated `maximized` state in `pane_grid` --- widget/src/pane_grid.rs | 60 +++++++++++++++++++---------------- widget/src/pane_grid/state.rs | 44 ++++++++++++++----------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 2644986f..adda79dd 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -92,8 +92,6 @@ use crate::core::{ Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use std::borrow::Cow; - const DRAG_DEADBAND_DISTANCE: f32 = 10.0; const THICKNESS_RATIO: f32 = 25.0; @@ -162,7 +160,6 @@ pub struct PaneGrid< internal: &'a state::Internal, panes: Vec, contents: Vec>, - maximized: Option, width: Length, height: Length, spacing: f32, @@ -189,8 +186,8 @@ where let contents = state .panes .iter() - .map(|(pane, pane_state)| match &state.maximized { - Some(p) if pane == p => view(*pane, pane_state, true), + .map(|(pane, pane_state)| match state.maximized() { + Some(p) if *pane == p => view(*pane, pane_state, true), _ => view(*pane, pane_state, false), }) .collect(); @@ -199,7 +196,6 @@ where internal: &state.internal, panes, contents, - maximized: state.maximized, width: Length::Fill, height: Length::Fill, spacing: 0.0, @@ -244,7 +240,7 @@ where where F: 'a + Fn(DragEvent) -> Message, { - if self.maximized.is_none() { + if self.internal.maximized().is_none() { self.on_drag = Some(Box::new(f)); } self @@ -263,7 +259,7 @@ where where F: 'a + Fn(ResizeEvent) -> Message, { - if self.maximized.is_none() { + if self.internal.maximized().is_none() { self.on_resize = Some((leeway.into().0, Box::new(f))); } self @@ -291,17 +287,12 @@ where } fn drag_enabled(&self) -> bool { - (self.maximized.is_none()) + self.internal + .maximized() + .is_none() .then(|| self.on_drag.is_some()) .unwrap_or_default() } - - fn node(&self) -> Cow<'_, Node> { - match self.maximized { - Some(pane) => Cow::Owned(Node::Pane(pane)), - None => Cow::Borrowed(&self.internal.layout), - } - } } impl<'a, Message, Theme, Renderer> Widget @@ -351,7 +342,6 @@ where ); let state::Widget { panes, .. } = tree.state.downcast_mut(); - panes.clone_from(&self.panes); } @@ -369,7 +359,7 @@ where limits: &layout::Limits, ) -> layout::Node { let size = limits.resolve(self.width, self.height, Size::ZERO); - let regions = self.node().pane_regions(self.spacing, size); + let regions = self.internal.layout().pane_regions(self.spacing, size); let children = self .panes @@ -378,7 +368,11 @@ where .zip(&self.contents) .zip(tree.children.iter_mut()) .filter_map(|((pane, content), tree)| { - if self.maximized.is_some() && Some(pane) != self.maximized { + if self + .internal + .maximized() + .is_some_and(|maximized| maximized != pane) + { return Some(layout::Node::new(Size::ZERO)); } @@ -413,7 +407,9 @@ where .zip(&mut tree.children) .zip(layout.children()) .filter(|(((pane, _), _), _)| { - self.maximized.map_or(true, |maximized| *pane == maximized) + self.internal + .maximized() + .map_or(true, |maximized| *pane == maximized) }) .for_each(|(((_, content), state), layout)| { content.operate(state, layout, renderer, operation); @@ -435,7 +431,7 @@ where let mut event_status = event::Status::Ignored; let state::Widget { action, .. } = tree.state.downcast_mut(); - let node = self.node(); + let node = self.internal.layout(); let on_drag = if self.drag_enabled() { &self.on_drag @@ -616,7 +612,9 @@ where .zip(&mut tree.children) .zip(layout.children()) .filter(|(((pane, _), _), _)| { - self.maximized.map_or(true, |maximized| *pane == maximized) + self.internal + .maximized() + .map_or(true, |maximized| *pane == maximized) }) .map(|(((pane, content), tree), layout)| { let is_picked = picked_pane == Some(pane); @@ -651,7 +649,7 @@ where } let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); - let node = self.node(); + let node = self.internal.layout(); let resize_axis = action.picked_split().map(|(_, axis)| axis).or_else(|| { @@ -690,7 +688,9 @@ where .zip(&tree.children) .zip(layout.children()) .filter(|(((pane, _), _), _)| { - self.maximized.map_or(true, |maximized| *pane == maximized) + self.internal + .maximized() + .map_or(true, |maximized| *pane == maximized) }) .map(|(((_, content), tree), layout)| { content.mouse_interaction( @@ -718,7 +718,7 @@ where ) { let state::Widget { action, .. } = tree.state.downcast_ref::(); - let node = self.node(); + let node = self.internal.layout(); let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); let picked_pane = action.picked_pane().filter(|(_, origin)| { @@ -797,7 +797,9 @@ where .zip(&tree.children) .zip(layout.children()) .filter(|(((pane, _), _), _)| { - self.maximized.map_or(true, |maximized| maximized == *pane) + self.internal + .maximized() + .map_or(true, |maximized| maximized == *pane) }) { match picked_pane { @@ -940,7 +942,11 @@ where .zip(&mut tree.children) .zip(layout.children()) .filter_map(|(((pane, content), state), layout)| { - if self.maximized.is_some() && Some(pane) != self.maximized { + if self + .internal + .maximized() + .is_some_and(|maximized| maximized != pane) + { return None; } diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index b7aef67d..f7e8f750 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -6,6 +6,7 @@ use crate::pane_grid::{ Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, }; +use std::borrow::Cow; use std::collections::BTreeMap; /// The state of a [`PaneGrid`]. @@ -31,11 +32,6 @@ pub struct State { /// /// [`PaneGrid`]: super::PaneGrid pub internal: Internal, - - /// The maximized [`Pane`] of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: super::PaneGrid - pub(super) maximized: Option, } impl State { @@ -57,11 +53,7 @@ impl State { let internal = Internal::from_configuration(&mut panes, config.into(), 0); - State { - panes, - internal, - maximized: None, - } + State { panes, internal } } /// Returns the total amount of panes in the [`State`]. @@ -214,7 +206,7 @@ impl State { } let _ = self.panes.insert(new_pane, state); - let _ = self.maximized.take(); + let _ = self.internal.maximized.take(); Some((new_pane, new_split)) } @@ -319,8 +311,8 @@ impl State { /// Closes the given [`Pane`] and returns its internal state and its closest /// sibling, if it exists. pub fn close(&mut self, pane: Pane) -> Option<(T, Pane)> { - if self.maximized == Some(pane) { - let _ = self.maximized.take(); + if self.internal.maximized == Some(pane) { + let _ = self.internal.maximized.take(); } if let Some(sibling) = self.internal.layout.remove(pane) { @@ -335,7 +327,7 @@ impl State { /// /// [`PaneGrid`]: super::PaneGrid pub fn maximize(&mut self, pane: Pane) { - self.maximized = Some(pane); + self.internal.maximized = Some(pane); } /// Restore the currently maximized [`Pane`] to it's normal size. All panes @@ -343,14 +335,14 @@ impl State { /// /// [`PaneGrid`]: super::PaneGrid pub fn restore(&mut self) { - let _ = self.maximized.take(); + let _ = self.internal.maximized.take(); } /// Returns the maximized [`Pane`] of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid pub fn maximized(&self) -> Option { - self.maximized + self.internal.maximized } } @@ -359,8 +351,9 @@ impl State { /// [`PaneGrid`]: super::PaneGrid #[derive(Debug, Clone)] pub struct Internal { - pub(super) layout: Node, + layout: Node, last_id: usize, + maximized: Option, } impl Internal { @@ -406,7 +399,22 @@ impl Internal { } }; - Self { layout, last_id } + Self { + layout, + last_id, + maximized: None, + } + } + + pub(super) fn layout(&self) -> Cow<'_, Node> { + match self.maximized { + Some(pane) => Cow::Owned(Node::Pane(pane)), + None => Cow::Borrowed(&self.layout), + } + } + + pub(super) fn maximized(&self) -> Option { + self.maximized } } From 089e629f4103bbd248c5f80441774d6ce98680fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 24 Oct 2024 13:48:42 +0200 Subject: [PATCH 383/657] Fix `responsive` diffing when `Tree` is emptied by ancestors --- widget/src/lazy/responsive.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index a7a99f56..a6c40ab0 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -83,7 +83,10 @@ where new_size: Size, view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>, ) { - if self.size == new_size { + let is_tree_empty = + tree.tag == tree::Tag::stateless() && tree.children.is_empty(); + + if !is_tree_empty && self.size == new_size { return; } From 55504ffcd41a5f1c8c35889501337a729b6aac28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 24 Oct 2024 13:55:04 +0200 Subject: [PATCH 384/657] Rename `state::Widget` to `pane_grid::Memory` --- widget/src/pane_grid.rs | 25 +++++++++++++++---------- widget/src/pane_grid/state.rs | 6 ------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index adda79dd..b4ed4b64 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -295,6 +295,12 @@ where } } +#[derive(Default)] +struct Memory { + action: state::Action, + order: Vec, +} + impl<'a, Message, Theme, Renderer> Widget for PaneGrid<'a, Message, Theme, Renderer> where @@ -302,11 +308,11 @@ where Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(state::Widget::default()) + tree::State::new(Memory::default()) } fn children(&self) -> Vec { @@ -314,7 +320,7 @@ where } fn diff(&self, tree: &mut Tree) { - let state::Widget { panes, .. } = tree.state.downcast_ref(); + let Memory { order, .. } = tree.state.downcast_ref(); // `Pane` always increments and is iterated by Ord so new // states are always added at the end. We can simply remove @@ -325,7 +331,7 @@ where let mut i = 0; let mut j = 0; tree.children.retain(|_| { - let retain = self.panes.get(i) == panes.get(j); + let retain = self.panes.get(i) == order.get(j); if retain { i += 1; @@ -341,8 +347,8 @@ where Content::state, ); - let state::Widget { panes, .. } = tree.state.downcast_mut(); - panes.clone_from(&self.panes); + let Memory { order, .. } = tree.state.downcast_mut(); + order.clone_from(&self.panes); } fn size(&self) -> Size { @@ -430,7 +436,7 @@ where ) -> event::Status { let mut event_status = event::Status::Ignored; - let state::Widget { action, .. } = tree.state.downcast_mut(); + let Memory { action, .. } = tree.state.downcast_mut(); let node = self.internal.layout(); let on_drag = if self.drag_enabled() { @@ -642,7 +648,7 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let state::Widget { action, .. } = tree.state.downcast_ref(); + let Memory { action, .. } = tree.state.downcast_ref(); if action.picked_pane().is_some() { return mouse::Interaction::Grabbing; @@ -716,8 +722,7 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let state::Widget { action, .. } = - tree.state.downcast_ref::(); + let Memory { action, .. } = tree.state.downcast_ref(); let node = self.internal.layout(); let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index f7e8f750..67cd3bf2 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -465,9 +465,3 @@ impl Action { } } } - -#[derive(Default)] -pub(super) struct Widget { - pub action: Action, - pub panes: Vec, -} From d08bc6e45d12c7a5e4037c20673ff832eb7802ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 24 Oct 2024 14:17:38 +0200 Subject: [PATCH 385/657] Add `relabel` helper to `pane_grid::State` --- widget/src/pane_grid/state.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 67cd3bf2..2f8a64ea 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -220,12 +220,8 @@ impl State { ) { if let Some((state, _)) = self.close(pane) { if let Some((new_pane, _)) = self.split(axis, target, state) { - // Ensure new node corresponds to original `Pane` for state continuity - self.swap(pane, new_pane); - let _ = self - .panes - .remove(&new_pane) - .and_then(|state| self.panes.insert(pane, state)); + // Ensure new node corresponds to original closed `Pane` for state continuity + self.relabel(new_pane, pane); if swap { self.swap(target, pane); @@ -258,22 +254,27 @@ impl State { &mut self, axis: Axis, pane: Pane, - swap: bool, + inverse: bool, ) { if let Some((state, _)) = self.close(pane) { if let Some((new_pane, _)) = - self.split_node(axis, None, state, swap) + self.split_node(axis, None, state, inverse) { - // Ensure new node corresponds to original `Pane` for state continuity - self.swap(pane, new_pane); - let _ = self - .panes - .remove(&new_pane) - .and_then(|state| self.panes.insert(pane, state)); + // Ensure new node corresponds to original closed `Pane` for state continuity + self.relabel(new_pane, pane); } } } + fn relabel(&mut self, target: Pane, label: Pane) { + self.swap(target, label); + + let _ = self + .panes + .remove(&target) + .and_then(|state| self.panes.insert(label, state)); + } + /// Swaps the position of the provided panes in the [`State`]. /// /// If you want to swap panes on drag and drop in your [`PaneGrid`], you From 6ed88f7608c952ca641812ae71edcf8a92d264b9 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 24 Oct 2024 16:12:18 -0400 Subject: [PATCH 386/657] Prevent unintended keyboard input during focus. --- winit/src/conversion.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 5d0f8348..8e6f7aae 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -191,6 +191,8 @@ pub fn window_event( })) } }, + // Ignore keyboard presses/releases during window focus/unfocus + WindowEvent::KeyboardInput { is_synthetic, .. } if is_synthetic => None, WindowEvent::KeyboardInput { event, .. } => Some(Event::Keyboard({ let key = { #[cfg(not(target_arch = "wasm32"))] From f3733a811bb020bdffe2090f0391cecc9d976da0 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 26 Oct 2024 09:28:20 -0700 Subject: [PATCH 387/657] Use float total_cmp instead of partial_cmp to get a total order. Since Rust version 1.81, sort_by will panic if the provided comparison function does not implement a total order. See https://github.com/rust/lang/rust/issues/129561 for more details. The simplest fix seems to be to use total_cmp instead of partial_cmp. --- graphics/src/damage.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 17d60451..8aa42798 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -45,15 +45,12 @@ pub fn list( /// Groups the given damage regions that are close together inside the given /// bounds. pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { - use std::cmp::Ordering; - const AREA_THRESHOLD: f32 = 20_000.0; damage.sort_by(|a, b| { a.center() .distance(Point::ORIGIN) - .partial_cmp(&b.center().distance(Point::ORIGIN)) - .unwrap_or(Ordering::Equal) + .total_cmp(&b.center().distance(Point::ORIGIN)) }); let mut output = Vec::new(); From ebc4e17ba853616326bd3957e75c3e8053b96513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 5 Nov 2024 13:32:14 +0100 Subject: [PATCH 388/657] Update `wgpu` to `23.0` --- Cargo.toml | 4 ++-- examples/custom_shader/src/scene/pipeline.rs | 8 ++++---- examples/integration/src/scene.rs | 4 ++-- wgpu/src/color.rs | 4 ++-- wgpu/src/image/mod.rs | 4 ++-- wgpu/src/quad/gradient.rs | 4 ++-- wgpu/src/quad/solid.rs | 4 ++-- wgpu/src/triangle.rs | 8 ++++---- wgpu/src/triangle/msaa.rs | 4 ++-- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bee83d2e..2db66da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ cosmic-text = "0.12" dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "0d7ba1bba4dd71eb88d2cface5ce649db2413cb7" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "09712a70df7431e9a3b1ac1bbd4fb634096cb3b4" } guillotiere = "0.6" half = "2.2" image = { version = "0.24", default-features = false } @@ -183,7 +183,7 @@ wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3.69" web-time = "1.1" -wgpu = "22.0" +wgpu = "23.0" winapi = "0.3" window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 84a3e5e2..567ab00b 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -241,7 +241,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[Vertex::desc(), cube::Raw::desc()], compilation_options: wgpu::PipelineCompilationOptions::default(), @@ -261,7 +261,7 @@ impl Pipeline { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { @@ -493,7 +493,7 @@ impl DepthPipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), @@ -509,7 +509,7 @@ impl DepthPipeline { multisample: wgpu::MultisampleState::default(), fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState::REPLACE), diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 15f97e08..7ba551aa 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -72,13 +72,13 @@ fn build_pipeline( layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &vs_module, - entry_point: "main", + entry_point: Some("main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &fs_module, - entry_point: "main", + entry_point: Some("main"), targets: &[Some(wgpu::ColorTargetState { format: texture_format, blend: Some(wgpu::BlendState { diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index effac8da..0f2c202f 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -108,14 +108,14 @@ pub fn convert( layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default( ), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index cf83c3f2..caac0813 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -128,7 +128,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, @@ -158,7 +158,7 @@ impl Pipeline { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 207b0d73..3d4ca4db 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -124,7 +124,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "gradient_vs_main", + entry_point: Some("gradient_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, @@ -157,7 +157,7 @@ impl Pipeline { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "gradient_fs_main", + entry_point: Some("gradient_fs_main"), targets: &quad::color_target_state(format), compilation_options: wgpu::PipelineCompilationOptions::default(), diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 86f118d6..f3e85ce7 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -89,7 +89,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "solid_vs_main", + entry_point: Some("solid_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, @@ -119,7 +119,7 @@ impl Pipeline { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "solid_fs_main", + entry_point: Some("solid_fs_main"), targets: &quad::color_target_state(format), compilation_options: wgpu::PipelineCompilationOptions::default(), diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index fb858c10..b865047e 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -753,7 +753,7 @@ mod solid { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "solid_vs_main", + entry_point: Some("solid_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< mesh::SolidVertex2D, @@ -773,7 +773,7 @@ mod solid { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "solid_fs_main", + entry_point: Some("solid_fs_main"), targets: &[Some(triangle::fragment_target(format))], compilation_options: wgpu::PipelineCompilationOptions::default(), @@ -926,7 +926,7 @@ mod gradient { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "gradient_vs_main", + entry_point: Some("gradient_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< mesh::GradientVertex2D, @@ -955,7 +955,7 @@ mod gradient { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "gradient_fs_main", + entry_point: Some("gradient_fs_main"), targets: &[Some(triangle::fragment_target(format))], compilation_options: wgpu::PipelineCompilationOptions::default(), diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index ec06e747..0a5b134f 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -110,14 +110,14 @@ impl Blit { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some( From 5c33ce18ed8b12db9a6ba138112804761d26fddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 00:13:42 +0200 Subject: [PATCH 389/657] Draft `reactive-rendering` feature for `button` --- Cargo.toml | 26 +++---- examples/multi_window/Cargo.toml | 2 +- widget/src/button.rs | 124 ++++++++++++++++++------------- winit/Cargo.toml | 2 +- winit/src/program.rs | 95 ++++++++++++++++------- winit/src/program/state.rs | 5 +- 6 files changed, 162 insertions(+), 92 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2db66da2..bf85ada2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,20 +22,20 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"] -# Enable the `wgpu` GPU-accelerated renderer backend +default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme", "reactive-rendering"] +# Enables the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] -# Enable the `tiny-skia` software renderer backend +# Enables the `tiny-skia` software renderer backend tiny-skia = ["iced_renderer/tiny-skia"] -# Enables the `Image` widget +# Enables the `image` widget image = ["image-without-codecs", "image/default"] -# Enables the `Image` widget, without any built-in codecs of the `image` crate +# Enables the `image` widget, without any built-in codecs of the `image` crate image-without-codecs = ["iced_widget/image", "dep:image"] -# Enables the `Svg` widget +# Enables the `svg` widget svg = ["iced_widget/svg"] -# Enables the `Canvas` widget +# Enables the `canvas` widget canvas = ["iced_widget/canvas"] -# Enables the `QRCode` widget +# Enables the `qr_code` widget qr_code = ["iced_widget/qr_code"] # Enables the `markdown` widget markdown = ["iced_widget/markdown"] @@ -55,18 +55,18 @@ system = ["iced_winit/system"] web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU webgl = ["iced_renderer/webgl"] -# Enables the syntax `highlighter` module +# Enables syntax highligthing highlighter = ["iced_highlighter", "iced_widget/highlighter"] -# Enables experimental multi-window support. -multi-window = ["iced_winit/multi-window"] # Enables the advanced module advanced = ["iced_core/advanced", "iced_widget/advanced"] -# Enables embedding Fira Sans as the default font on Wasm builds +# Embeds Fira Sans as the default font on Wasm builds fira-sans = ["iced_renderer/fira-sans"] -# Enables auto-detecting light/dark mode for the built-in theme +# Auto-detects light/dark mode for the built-in theme auto-detect-theme = ["iced_core/auto-detect-theme"] # Enables strict assertions for debugging purposes at the expense of performance strict-assertions = ["iced_renderer/strict-assertions"] +# Redraws only when widgets react to some runtime event +reactive-rendering = ["iced_winit/reactive-rendering"] [dependencies] iced_core.workspace = true diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 2e222dfb..3f89417f 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug", "multi-window"] } +iced = { path = "../..", features = ["debug"] } diff --git a/widget/src/button.rs b/widget/src/button.rs index a3394a01..5850cea0 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -26,6 +26,7 @@ use crate::core::theme::palette; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; +use crate::core::window; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, @@ -81,6 +82,7 @@ where padding: Padding, clip: bool, class: Theme::Class<'a>, + status: Option, } enum OnPress<'a, Message> { @@ -117,6 +119,7 @@ where padding: DEFAULT_PADDING, clip: false, class: Theme::default(), + status: None, } } @@ -294,49 +297,85 @@ where return event::Status::Captured; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if self.on_press.is_some() { - let bounds = layout.bounds(); - - if cursor.is_over(bounds) { - let state = tree.state.downcast_mut::(); - - state.is_pressed = true; - - return event::Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.as_ref().map(OnPress::get) - { - let state = tree.state.downcast_mut::(); - - if state.is_pressed { - state.is_pressed = false; - + let mut update = || { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if self.on_press.is_some() { let bounds = layout.bounds(); if cursor.is_over(bounds) { - shell.publish(on_press); - } + let state = tree.state.downcast_mut::(); - return event::Status::Captured; + state.is_pressed = true; + + return event::Status::Captured; + } } } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = tree.state.downcast_mut::(); + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = + self.on_press.as_ref().map(OnPress::get) + { + let state = tree.state.downcast_mut::(); - state.is_pressed = false; + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = tree.state.downcast_mut::(); + + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored + }; + + let update_status = update(); + + let current_status = if self.on_press.is_none() { + Status::Disabled + } else if cursor.is_over(layout.bounds()) { + let state = tree.state.downcast_ref::(); + + if state.is_pressed { + Status::Pressed + } else { + Status::Hovered + } + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.status = Some(current_status); + } else { + match self.status { + Some(status) if status != current_status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} } - _ => {} } - event::Status::Ignored + update_status } fn draw( @@ -351,23 +390,8 @@ where ) { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); - let is_mouse_over = cursor.is_over(bounds); - - let status = if self.on_press.is_none() { - Status::Disabled - } else if is_mouse_over { - let state = tree.state.downcast_ref::(); - - if state.is_pressed { - Status::Pressed - } else { - Status::Hovered - } - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = + theme.style(&self.class, self.status.unwrap_or(Status::Disabled)); if style.background.is_some() || style.border.width > 0.0 diff --git a/winit/Cargo.toml b/winit/Cargo.toml index bd6feb00..b8f5a723 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -22,7 +22,7 @@ x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] -multi-window = ["iced_runtime/multi-window"] +reactive-rendering = [] [dependencies] iced_futures.workspace = true diff --git a/winit/src/program.rs b/winit/src/program.rs index 8d1eec3a..2ac7ad0d 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -691,6 +691,7 @@ async fn run_instance( let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); let mut clipboard = Clipboard::unconnected(); + let mut redraw_queue = Vec::new(); debug.startup_finished(); @@ -758,14 +759,30 @@ async fn run_instance( } Event::EventLoopAwakened(event) => { match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) => { + event::Event::NewEvents(event::StartCause::Init) => { for (_id, window) in window_manager.iter_mut() { window.raw.request_redraw(); } } + event::Event::NewEvents( + event::StartCause::ResumeTimeReached { .. }, + ) => { + let now = Instant::now(); + + while let Some((target, id)) = + redraw_queue.last().copied() + { + if target > now { + break; + } + + let _ = redraw_queue.pop(); + + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_redraw(); + } + } + } event::Event::PlatformSpecific( event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), @@ -857,23 +874,19 @@ async fn run_instance( status: core::event::Status::Ignored, }); - let _ = control_sender.start_send(Control::ChangeFlow( - match ui_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - window.raw.request_redraw(); - - ControlFlow::Wait - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }, - )); + if let user_interface::State::Updated { + redraw_request: Some(redraw_request), + } = ui_state + { + match redraw_request { + window::RedrawRequest::NextFrame => { + window.raw.request_redraw(); + } + window::RedrawRequest::At(at) => { + redraw_queue.push((at, id)); + } + } + } let physical_size = window.state.physical_size(); @@ -1065,13 +1078,25 @@ async fn run_instance( &mut messages, ); + #[cfg(not(feature = "reactive-rendering"))] window.raw.request_redraw(); - if !uis_stale { - uis_stale = matches!( - ui_state, - user_interface::State::Outdated - ); + match ui_state { + #[cfg(feature = "reactive-rendering")] + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + window.raw.request_redraw(); + } + window::RedrawRequest::At(at) => { + redraw_queue.push((at, id)); + } + }, + user_interface::State::Outdated => { + uis_stale = true; + } + user_interface::State::Updated { .. } => {} } for (event, status) in window_events @@ -1139,6 +1164,24 @@ async fn run_instance( actions = 0; } } + + if !redraw_queue.is_empty() { + redraw_queue.sort_by( + |(target_a, _), (target_b, _)| { + target_a.cmp(target_b).reverse() + }, + ); + + let (target, _id) = redraw_queue + .last() + .copied() + .expect("Redraw queue is not empty"); + + let _ = + control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(target), + )); + } } _ => {} } diff --git a/winit/src/program/state.rs b/winit/src/program/state.rs index a7fa2788..b8a58960 100644 --- a/winit/src/program/state.rs +++ b/winit/src/program/state.rs @@ -190,7 +190,10 @@ where .. }, .. - } => _debug.toggle(), + } => { + _debug.toggle(); + window.request_redraw(); + } _ => {} } } From 97bcca04002d9d7c4812e178d30fb12358fad72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 00:25:30 +0200 Subject: [PATCH 390/657] Remove `TODO` about reactive rendering in `iced_winit` --- winit/src/program.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index 2ac7ad0d..f15e5be5 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -824,11 +824,6 @@ async fn run_instance( continue; }; - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. let redraw_event = core::Event::Window( window::Event::RedrawRequested(Instant::now()), ); From 3ba7c71e3ffb651fde753bcf63bb604c16d4bcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 00:45:36 +0200 Subject: [PATCH 391/657] Implement `reactive-rendering` for `slider` --- widget/src/slider.rs | 294 +++++++++++++++++++++++-------------------- winit/src/program.rs | 2 + 2 files changed, 162 insertions(+), 134 deletions(-) diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 31aa0e0c..25f0d85f 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -37,6 +37,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Widget, @@ -95,6 +96,7 @@ where width: Length, height: f32, class: Theme::Class<'a>, + status: Option, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> @@ -141,6 +143,7 @@ where width: Length::Fill, height: Self::DEFAULT_HEIGHT, class: Theme::default(), + status: None, } } @@ -253,16 +256,40 @@ where ) -> event::Status { let state = tree.state.downcast_mut::(); - let is_dragging = state.is_dragging; - let current_value = self.value; + let mut update = || { + let current_value = self.value; - let locate = |cursor_position: Point| -> Option { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - Some(*self.range.start()) - } else if cursor_position.x >= bounds.x + bounds.width { - Some(*self.range.end()) - } else { + let locate = |cursor_position: Point| -> Option { + let bounds = layout.bounds(); + + let new_value = if cursor_position.x <= bounds.x { + Some(*self.range.start()) + } else if cursor_position.x >= bounds.x + bounds.width { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + T::from_f64(value.min(end)) + }; + + new_value + }; + + let increment = |value: T| -> Option { let step = if state.keyboard_modifiers.shift() { self.shift_step.unwrap_or(self.step) } else { @@ -270,168 +297,167 @@ where } .into(); - let start = (*self.range.start()).into(); - let end = (*self.range.end()).into(); + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - T::from_f64(value.min(end)) + T::from_f64(new_value) }; - new_value - }; + let decrement = |value: T| -> Option { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); - let increment = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - } - .into(); + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); - let steps = (value.into() / step).round(); - let new_value = step * (steps + 1.0); + if new_value < (*self.range.start()).into() { + return Some(*self.range.start()); + } - if new_value > (*self.range.end()).into() { - return Some(*self.range.end()); - } + T::from_f64(new_value) + }; - T::from_f64(new_value) - }; + let change = |new_value: T| { + if (self.value.into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((self.on_change)(new_value)); - let decrement = |value: T| -> Option { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - } - .into(); + self.value = new_value; + } + }; - let steps = (value.into() / step).round(); - let new_value = step * (steps - 1.0); + match &event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = + cursor.position_over(layout.bounds()) + { + if state.keyboard_modifiers.command() { + let _ = self.default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(change); + state.is_dragging = true; + } - if new_value < (*self.range.start()).into() { - return Some(*self.range.start()); - } - - T::from_f64(new_value) - }; - - let change = |new_value: T| { - if (self.value.into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((self.on_change)(new_value)); - - self.value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = - cursor.position_over(layout.bounds()) - { - if state.keyboard_modifiers.command() { - let _ = self.default.map(change); + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + shell.publish(on_release); + } state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; + + return event::Status::Captured; } - - return event::Status::Captured; } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = self.on_release.clone() { - shell.publish(on_release); + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if state.is_dragging { + let _ = cursor.position().and_then(locate).map(change); + + return event::Status::Captured; } - state.is_dragging = false; - - return event::Status::Captured; } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - let _ = cursor.position().and_then(locate).map(change); + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if state.keyboard_modifiers.control() => + { + if cursor.is_over(layout.bounds()) { + let delta = match delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::WheelScrolled { delta }) - if state.keyboard_modifiers.control() => - { - if cursor.is_over(layout.bounds()) { - let delta = match delta { - mouse::ScrollDelta::Lines { x: _, y } => y, - mouse::ScrollDelta::Pixels { x: _, y } => y, - }; - - if delta < 0.0 { - let _ = decrement(current_value).map(change); - } else { - let _ = increment(current_value).map(change); - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.is_over(layout.bounds()) { - match key { - Key::Named(key::Named::ArrowUp) => { + if *delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { let _ = increment(current_value).map(change); } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), - } - return event::Status::Captured; + return event::Status::Captured; + } } + Event::Keyboard(keyboard::Event::KeyPressed { + key, .. + }) => { + if cursor.is_over(layout.bounds()) { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged( + modifiers, + )) => { + state.keyboard_modifiers = *modifiers; + } + _ => {} } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; + + event::Status::Ignored + }; + + let update_status = update(); + + let current_status = if state.is_dragging { + Status::Dragged + } else if cursor.is_over(layout.bounds()) { + Status::Hovered + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.status = Some(current_status); + } else { + match self.status { + Some(status) if status != current_status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} } - _ => {} } - event::Status::Ignored + update_status } fn draw( &self, - tree: &Tree, + _tree: &Tree, renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - let style = theme.style( - &self.class, - if state.is_dragging { - Status::Dragged - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }, - ); + let style = + theme.style(&self.class, self.status.unwrap_or(Status::Active)); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { diff --git a/winit/src/program.rs b/winit/src/program.rs index f15e5be5..579038af 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1161,6 +1161,8 @@ async fn run_instance( } if !redraw_queue.is_empty() { + // The queue should be fairly short, so we can + // simply sort all of the time. redraw_queue.sort_by( |(target_a, _), (target_b, _)| { target_a.cmp(target_b).reverse() From 52490397d64f187d55f51dc5883e3ba6c0da57a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 02:50:46 +0200 Subject: [PATCH 392/657] Implement `reactive-rendering` for `text_input` ... and fix the redraw queue logic in `iced_winit`. --- widget/src/text_input.rs | 221 +++++++++++++++++++++------- widget/src/text_input/cursor.rs | 4 +- winit/src/program.rs | 48 +++--- winit/src/program/window_manager.rs | 16 ++ 4 files changed, 201 insertions(+), 88 deletions(-) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index ff413779..c18009a2 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -120,6 +120,7 @@ pub struct TextInput< on_submit: Option, icon: Option>, class: Theme::Class<'a>, + last_status: Option, } /// The default [`Padding`] of a [`TextInput`]. @@ -150,6 +151,7 @@ where on_submit: None, icon: None, class: Theme::default(), + last_status: None, } } @@ -400,7 +402,7 @@ where renderer: &mut Renderer, theme: &Theme, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, value: Option<&Value>, viewport: &Rectangle, ) { @@ -416,19 +418,8 @@ where let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let status = if is_disabled { - Status::Disabled - } else if state.is_focused() { - Status::Focused - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Disabled)); renderer.fill_quad( renderer::Quad { @@ -660,22 +651,21 @@ where ); }; - match event { + match &event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state::(tree); + let cursor_before = state.cursor; let click_position = cursor.position_over(layout.bounds()); state.is_focused = if click_position.is_some() { - state.is_focused.or_else(|| { - let now = Instant::now(); + let now = Instant::now(); - Some(Focus { - updated_at: now, - now, - is_window_focused: true, - }) + Some(Focus { + updated_at: now, + now, + is_window_focused: true, }) } else { None @@ -760,6 +750,10 @@ where state.last_click = Some(click); + if cursor_before != state.cursor { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + return event::Status::Captured; } } @@ -801,10 +795,20 @@ where ) .unwrap_or(0); + let selection_before = state.cursor.selection(&value); + state .cursor .select_range(state.cursor.start(&value), position); + if let Some(focus) = &mut state.is_focused { + focus.updated_at = Instant::now(); + } + + if selection_before != state.cursor.selection(&value) { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + return event::Status::Captured; } } @@ -815,7 +819,6 @@ where if let Some(focus) = &mut state.is_focused { let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); match key.as_ref() { keyboard::Key::Character("c") @@ -857,6 +860,7 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + focus.updated_at = Instant::now(); update_cache(state, &self.value); return event::Status::Captured; @@ -885,7 +889,6 @@ where let mut editor = Editor::new(&mut self.value, &mut state.cursor); - editor.paste(content.clone()); let message = if let Some(paste) = &self.on_paste { @@ -896,7 +899,7 @@ where shell.publish(message); state.is_pasting = Some(content); - + focus.updated_at = Instant::now(); update_cache(state, &self.value); return event::Status::Captured; @@ -904,8 +907,18 @@ where keyboard::Key::Character("a") if state.keyboard_modifiers.command() => { + let cursor_before = state.cursor; + state.cursor.select_all(&self.value); + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + return event::Status::Captured; } _ => {} @@ -930,7 +943,6 @@ where shell.publish(message); focus.updated_at = Instant::now(); - update_cache(state, &self.value); return event::Status::Captured; @@ -941,6 +953,8 @@ where keyboard::Key::Named(key::Named::Enter) => { if let Some(on_submit) = self.on_submit.clone() { shell.publish(on_submit); + + return event::Status::Captured; } } keyboard::Key::Named(key::Named::Backspace) => { @@ -969,7 +983,10 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + focus.updated_at = Instant::now(); update_cache(state, &self.value); + + return event::Status::Captured; } keyboard::Key::Named(key::Named::Delete) => { let Some(on_input) = &self.on_input else { @@ -1000,9 +1017,14 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + focus.updated_at = Instant::now(); update_cache(state, &self.value); + + return event::Status::Captured; } keyboard::Key::Named(key::Named::Home) => { + let cursor_before = state.cursor; + if modifiers.shift() { state.cursor.select_range( state.cursor.start(&self.value), @@ -1011,8 +1033,20 @@ where } else { state.cursor.move_to(0); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + + return event::Status::Captured; } keyboard::Key::Named(key::Named::End) => { + let cursor_before = state.cursor; + if modifiers.shift() { state.cursor.select_range( state.cursor.start(&self.value), @@ -1021,10 +1055,22 @@ where } else { state.cursor.move_to(self.value.len()); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + + return event::Status::Captured; } keyboard::Key::Named(key::Named::ArrowLeft) if modifiers.macos_command() => { + let cursor_before = state.cursor; + if modifiers.shift() { state.cursor.select_range( state.cursor.start(&self.value), @@ -1033,10 +1079,22 @@ where } else { state.cursor.move_to(0); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + + return event::Status::Captured; } keyboard::Key::Named(key::Named::ArrowRight) if modifiers.macos_command() => { + let cursor_before = state.cursor; + if modifiers.shift() { state.cursor.select_range( state.cursor.start(&self.value), @@ -1045,8 +1103,20 @@ where } else { state.cursor.move_to(self.value.len()); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + + return event::Status::Captured; } keyboard::Key::Named(key::Named::ArrowLeft) => { + let cursor_before = state.cursor; + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state @@ -1062,8 +1132,20 @@ where } else { state.cursor.move_left(&self.value); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + + return event::Status::Captured; } keyboard::Key::Named(key::Named::ArrowRight) => { + let cursor_before = state.cursor; + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state @@ -1079,6 +1161,16 @@ where } else { state.cursor.move_right(&self.value); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); + } + + return event::Status::Captured; } keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; @@ -1087,39 +1179,22 @@ where state.keyboard_modifiers = keyboard::Modifiers::default(); - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; + + return event::Status::Captured; } _ => {} } - - return event::Status::Captured; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state::(tree); if state.is_focused.is_some() { - match key.as_ref() { - keyboard::Key::Character("v") => { - state.is_pasting = None; - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } + if let keyboard::Key::Character("v") = key.as_ref() { + state.is_pasting = None; - return event::Status::Captured; + return event::Status::Captured; + } } state.is_pasting = None; @@ -1127,7 +1202,7 @@ where Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state::(tree); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; } Event::Window(window::Event::Unfocused) => { let state = state::(tree); @@ -1150,15 +1225,20 @@ where let state = state::(tree); if let Some(focus) = &mut state.is_focused { - if focus.is_window_focused { - focus.now = now; + if focus.is_window_focused + && matches!( + state.cursor.state(&self.value), + cursor::State::Index(_) + ) + { + focus.now = *now; let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() + - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis( + *now + Duration::from_millis( millis_until_redraw as u64, ), )); @@ -1168,6 +1248,32 @@ where _ => {} } + let state = state::(tree); + let is_disabled = self.on_input.is_none(); + + let status = if is_disabled { + Status::Disabled + } else if state.is_focused() { + Status::Focused { + is_hovered: cursor.is_over(layout.bounds()), + } + } else if cursor.is_over(layout.bounds()) { + Status::Hovered + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(status); + } else { + match self.last_status { + Some(last_status) if status != last_status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} + } + } + event::Status::Ignored } @@ -1535,7 +1641,10 @@ pub enum Status { /// The [`TextInput`] is being hovered. Hovered, /// The [`TextInput`] is focused. - Focused, + Focused { + /// Whether the [`TextInput`] is hovered, while focused. + is_hovered: bool, + }, /// The [`TextInput`] cannot be interacted with. Disabled, } @@ -1612,7 +1721,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, ..active }, - Status::Focused => Style { + Status::Focused { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border diff --git a/widget/src/text_input/cursor.rs b/widget/src/text_input/cursor.rs index f682b17d..a326fc8f 100644 --- a/widget/src/text_input/cursor.rs +++ b/widget/src/text_input/cursor.rs @@ -2,13 +2,13 @@ use crate::text_input::Value; /// The cursor of a text input. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Cursor { state: State, } /// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum State { /// Cursor without a selection Index(usize), diff --git a/winit/src/program.rs b/winit/src/program.rs index 579038af..a6729fa0 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -691,7 +691,6 @@ async fn run_instance( let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); let mut clipboard = Clipboard::unconnected(); - let mut redraw_queue = Vec::new(); debug.startup_finished(); @@ -769,17 +768,12 @@ async fn run_instance( ) => { let now = Instant::now(); - while let Some((target, id)) = - redraw_queue.last().copied() - { - if target > now { - break; - } - - let _ = redraw_queue.pop(); - - if let Some(window) = window_manager.get_mut(id) { - window.raw.request_redraw(); + for (_id, window) in window_manager.iter_mut() { + if let Some(redraw_at) = window.redraw_at { + if redraw_at <= now { + window.raw.request_redraw(); + window.redraw_at = None; + } } } } @@ -878,7 +872,7 @@ async fn run_instance( window.raw.request_redraw(); } window::RedrawRequest::At(at) => { - redraw_queue.push((at, id)); + window.redraw_at = Some(at); } } } @@ -1039,7 +1033,10 @@ async fn run_instance( } } event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { + if events.is_empty() + && messages.is_empty() + && window_manager.is_idle() + { continue; } @@ -1085,7 +1082,7 @@ async fn run_instance( window.raw.request_redraw(); } window::RedrawRequest::At(at) => { - redraw_queue.push((at, id)); + window.redraw_at = Some(at); } }, user_interface::State::Outdated => { @@ -1160,24 +1157,15 @@ async fn run_instance( } } - if !redraw_queue.is_empty() { - // The queue should be fairly short, so we can - // simply sort all of the time. - redraw_queue.sort_by( - |(target_a, _), (target_b, _)| { - target_a.cmp(target_b).reverse() - }, - ); - - let (target, _id) = redraw_queue - .last() - .copied() - .expect("Redraw queue is not empty"); - + if let Some(redraw_at) = window_manager.redraw_at() { let _ = control_sender.start_send(Control::ChangeFlow( - ControlFlow::WaitUntil(target), + ControlFlow::WaitUntil(redraw_at), )); + } else { + let _ = control_sender.start_send( + Control::ChangeFlow(ControlFlow::Wait), + ); } } _ => {} diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 3d22e155..7c00a84b 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -1,4 +1,5 @@ use crate::core::mouse; +use crate::core::time::Instant; use crate::core::window::Id; use crate::core::{Point, Size}; use crate::graphics::Compositor; @@ -62,6 +63,7 @@ where surface, renderer, mouse_interaction: mouse::Interaction::None, + redraw_at: None, }, ); @@ -74,6 +76,19 @@ where self.entries.is_empty() } + pub fn is_idle(&self) -> bool { + self.entries + .values() + .any(|window| window.redraw_at.is_some()) + } + + pub fn redraw_at(&self) -> Option { + self.entries + .values() + .filter_map(|window| window.redraw_at) + .min() + } + pub fn first(&self) -> Option<&Window> { self.entries.first_key_value().map(|(_id, window)| window) } @@ -138,6 +153,7 @@ where pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, pub renderer: P::Renderer, + pub redraw_at: Option, } impl Window From 0691e617f31aab92cb5ddc4698f841357f4c14ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 02:53:30 +0200 Subject: [PATCH 393/657] Fix `WindowManager::is_idle` in `iced_winit` --- winit/src/program/window_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 7c00a84b..10a973fe 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -79,7 +79,7 @@ where pub fn is_idle(&self) -> bool { self.entries .values() - .any(|window| window.redraw_at.is_some()) + .all(|window| window.redraw_at.is_none()) } pub fn redraw_at(&self) -> Option { From f6c322f2f9aa5dd0ea151c702b2ba4754d68e550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 12:10:24 +0200 Subject: [PATCH 394/657] Implement `reactive-rendering` for `checkbox` --- widget/src/checkbox.rs | 49 ++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 819f0d9d..e5dea3cc 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -40,6 +40,7 @@ use crate::core::theme::palette; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget, @@ -100,6 +101,7 @@ pub struct Checkbox< font: Option, icon: Icon, class: Theme::Class<'a>, + last_status: Option, } impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> @@ -139,6 +141,7 @@ where shaping: text::Shaping::Basic, }, class: Theme::default(), + last_status: None, } } @@ -326,6 +329,31 @@ where _ => {} } + let current_status = { + let is_mouse_over = cursor.is_over(layout.bounds()); + let is_disabled = self.on_toggle.is_none(); + let is_checked = self.is_checked; + + if is_disabled { + Status::Disabled { is_checked } + } else if is_mouse_over { + Status::Hovered { is_checked } + } else { + Status::Active { is_checked } + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(current_status); + } else { + match self.last_status { + Some(status) if status != current_status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} + } + } + event::Status::Ignored } @@ -351,24 +379,17 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let is_mouse_over = cursor.is_over(layout.bounds()); - let is_disabled = self.on_toggle.is_none(); - let is_checked = self.is_checked; - let mut children = layout.children(); - let status = if is_disabled { - Status::Disabled { is_checked } - } else if is_mouse_over { - Status::Hovered { is_checked } - } else { - Status::Active { is_checked } - }; - - let style = theme.style(&self.class, status); + let style = theme.style( + &self.class, + self.last_status.unwrap_or(Status::Disabled { + is_checked: self.is_checked, + }), + ); { let layout = children.next().unwrap(); From 0c7770218706c2b1f3d27dd4ea2bc18f489a5ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 12:18:23 +0200 Subject: [PATCH 395/657] Implement `reactive-rendering` for `radio` --- widget/src/radio.rs | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/widget/src/radio.rs b/widget/src/radio.rs index d2a3bd6a..714d4fb5 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -66,6 +66,7 @@ use crate::core::text; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget, @@ -147,6 +148,7 @@ where text_wrapping: text::Wrapping, font: Option, class: Theme::Class<'a>, + last_status: Option, } impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> @@ -192,6 +194,7 @@ where text_wrapping: text::Wrapping::default(), font: None, class: Theme::default(), + last_status: None, } } @@ -344,6 +347,28 @@ where _ => {} } + let current_status = { + let is_mouse_over = cursor.is_over(layout.bounds()); + let is_selected = self.is_selected; + + if is_mouse_over { + Status::Hovered { is_selected } + } else { + Status::Active { is_selected } + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(current_status); + } else { + match self.last_status { + Some(status) if status != current_status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} + } + } + event::Status::Ignored } @@ -369,21 +394,17 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let is_mouse_over = cursor.is_over(layout.bounds()); - let is_selected = self.is_selected; - let mut children = layout.children(); - let status = if is_mouse_over { - Status::Hovered { is_selected } - } else { - Status::Active { is_selected } - }; - - let style = theme.style(&self.class, status); + let style = theme.style( + &self.class, + self.last_status.unwrap_or(Status::Active { + is_selected: self.is_selected, + }), + ); { let layout = children.next().unwrap(); From 46017c6483714a8245c090680e4810a621493f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Oct 2024 12:26:12 +0200 Subject: [PATCH 396/657] Implement `reactive-rendering` for `toggler` --- widget/src/toggler.rs | 49 ++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index fdd2e68c..13244e34 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -39,6 +39,7 @@ use crate::core::text; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget, @@ -99,6 +100,7 @@ pub struct Toggler< spacing: f32, font: Option, class: Theme::Class<'a>, + last_status: Option, } impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> @@ -132,6 +134,7 @@ where spacing: Self::DEFAULT_SIZE / 2.0, font: None, class: Theme::default(), + last_status: None, } } @@ -319,7 +322,7 @@ where return event::Status::Ignored; }; - match event { + let event_status = match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); @@ -333,7 +336,32 @@ where } } _ => event::Status::Ignored, + }; + + let current_status = if self.on_toggle.is_none() { + Status::Disabled + } else if cursor.is_over(layout.bounds()) { + Status::Hovered { + is_toggled: self.is_toggled, + } + } else { + Status::Active { + is_toggled: self.is_toggled, + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(current_status); + } else { + match self.last_status { + Some(status) if status != current_status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} + } } + + event_status } fn mouse_interaction( @@ -362,7 +390,7 @@ where theme: &Theme, style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { /// Makes sure that the border radius of the toggler looks good at every size. @@ -391,21 +419,8 @@ where } let bounds = toggler_layout.bounds(); - let is_mouse_over = cursor.is_over(layout.bounds()); - - let status = if self.on_toggle.is_none() { - Status::Disabled - } else if is_mouse_over { - Status::Hovered { - is_toggled: self.is_toggled, - } - } else { - Status::Active { - is_toggled: self.is_toggled, - } - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Disabled)); let border_radius = bounds.height / BORDER_RADIUS_RATIO; let space = SPACE_RATIO * bounds.height; From 7908b6eba91b91c61f7839b3d52fbee124b55cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 23 Oct 2024 19:37:28 +0200 Subject: [PATCH 397/657] Request a redraw when a window is resized If we do not request it, macOS does not get any `RedrawRequested` events. Shouldn't `winit` [take care of this]? Probably a bug. [take care of this]: https://docs.rs/winit/0.30.5/winit/event/enum.WindowEvent.html#variant.RedrawRequested --- winit/src/program.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/winit/src/program.rs b/winit/src/program.rs index a6729fa0..fb30ccd9 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -995,6 +995,13 @@ async fn run_instance( continue; }; + if matches!( + window_event, + winit::event::WindowEvent::Resized(_) + ) { + window.raw.request_redraw(); + } + if matches!( window_event, winit::event::WindowEvent::CloseRequested From fdf046daff5e9196713d2513cd1b654cfc122d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 23 Oct 2024 19:49:19 +0200 Subject: [PATCH 398/657] Implement `reactive-rendering` for `pick_list` --- widget/src/pick_list.rs | 57 ++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 4f1e9da9..ec1c054f 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -71,6 +71,7 @@ use crate::core::text::paragraph; use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Background, Border, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, @@ -173,6 +174,7 @@ pub struct PickList< handle: Handle, class: ::Class<'a>, menu_class: ::Class<'a>, + last_status: Option, } impl<'a, T, L, V, Message, Theme, Renderer> @@ -208,6 +210,7 @@ where handle: Handle::default(), class: ::default(), menu_class: ::default_menu(), + last_status: None, } } @@ -436,12 +439,11 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - match event { + let state = tree.state.downcast_mut::>(); + + let event_status = match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = - tree.state.downcast_mut::>(); - if state.is_open { // Event wasn't processed by overlay, so cursor was clicked either outside its // bounds or on the drop-down, either way we close the overlay. @@ -474,9 +476,6 @@ where Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Lines { y, .. }, }) => { - let state = - tree.state.downcast_mut::>(); - if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !state.is_open @@ -519,15 +518,33 @@ where } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = - tree.state.downcast_mut::>(); - state.keyboard_modifiers = modifiers; event::Status::Ignored } _ => event::Status::Ignored, + }; + + let status = if state.is_open { + Status::Opened + } else if cursor.is_over(layout.bounds()) { + Status::Hovered + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(status); + } else { + match self.last_status { + Some(last_status) if last_status != status => { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + _ => {} + } } + + event_status } fn mouse_interaction( @@ -555,7 +572,7 @@ where theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -563,18 +580,12 @@ where let state = tree.state.downcast_ref::>(); let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - let is_selected = selected.is_some(); - let status = if state.is_open { - Status::Opened - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }; - - let style = Catalog::style(theme, &self.class, status); + let style = Catalog::style( + theme, + &self.class, + self.last_status.unwrap_or(Status::Active), + ); renderer.fill_quad( renderer::Quad { @@ -671,7 +682,7 @@ where wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), - if is_selected { + if selected.is_some() { style.text_color } else { style.placeholder_color From 908af3fed72131d21d7c7ffbc503c9b1e8a65144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 23 Oct 2024 20:04:13 +0200 Subject: [PATCH 399/657] Implement `reactive-rendering` for `menu` --- widget/src/overlay/menu.rs | 43 ++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b641e8f5..e79bd3da 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -8,7 +8,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::text::{self, Text}; use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Background, Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, Size, Theme, Vector, @@ -334,6 +335,10 @@ where class: &'a ::Class<'b>, } +struct ListState { + is_hovered: Option, +} + impl<'a, 'b, T, Message, Theme, Renderer> Widget for List<'a, 'b, T, Message, Theme, Renderer> where @@ -341,6 +346,14 @@ where Theme: Catalog, Renderer: text::Renderer, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(ListState { is_hovered: None }) + } + fn size(&self) -> Size { Size { width: Length::Fill, @@ -376,7 +389,7 @@ where fn on_event( &mut self, - _state: &mut Tree, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -411,14 +424,20 @@ where let new_hovered_option = (cursor_position.y / option_height) as usize; - if let Some(on_option_hovered) = self.on_option_hovered { - if *self.hovered_option != Some(new_hovered_option) { - if let Some(option) = - self.options.get(new_hovered_option) + if *self.hovered_option != Some(new_hovered_option) { + if let Some(option) = + self.options.get(new_hovered_option) + { + if let Some(on_option_hovered) = + self.on_option_hovered { shell .publish(on_option_hovered(option.clone())); } + + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); } } @@ -451,6 +470,18 @@ where _ => {} } + let state = tree.state.downcast_mut::(); + + if state.is_hovered.is_some_and(|is_hovered| { + is_hovered != cursor.is_over(layout.bounds()) + }) { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + state.is_hovered = Some(cursor.is_over(layout.bounds())); + } + event::Status::Ignored } From 7fbc195b11f9a858bcc8f56f76907af82c966c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 23 Oct 2024 21:07:45 +0200 Subject: [PATCH 400/657] Implement `reactive-rendering` for `scrollable` --- widget/src/scrollable.rs | 687 +++++++++++++++++++++------------------ 1 file changed, 362 insertions(+), 325 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 528d63c1..c4350547 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -81,6 +81,7 @@ pub struct Scrollable< content: Element<'a, Message, Theme, Renderer>, on_scroll: Option Message + 'a>>, class: Theme::Class<'a>, + last_status: Option, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> @@ -108,6 +109,7 @@ where content: content.into(), on_scroll: None, class: Theme::default(), + last_status: None, } .validate() } @@ -531,6 +533,8 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); + let last_offsets = (state.offset_x, state.offset_y); + if let Some(last_scrolled) = state.last_scrolled { let clear_transaction = match event { Event::Mouse( @@ -549,309 +553,65 @@ where } } - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_y_scroller(cursor_position), - scrollbars.y, - ) { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_x_scroller(cursor_position), - scrollbars.x, - ) { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } - - let content_status = if state.last_scrolled.is_some() - && matches!(event, Event::Mouse(mouse::Event::WheelScrolled { .. })) - { - event::Status::Ignored - } else { - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available( - cursor_position - + state.translation( - self.direction, - bounds, - content_bounds, - ), - ) - } - _ => mouse::Cursor::Unavailable, - }; - - let translation = - state.translation(self.direction, bounds, content_bounds); - - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event.clone(), - content, - cursor, - renderer, - clipboard, - shell, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ) - }; - - if matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch( - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } - ) - ) { - state.scroll_area_touched_at = None; - state.x_scroller_grabbed_at = None; - state.y_scroller_grabbed_at = None; - - return content_status; - } - - if let event::Status::Captured = content_status { - return event::Status::Captured; - } - - if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = - event - { - state.keyboard_modifiers = modifiers; - - return event::Status::Ignored; - } - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if cursor_over_scrollable.is_none() { - return event::Status::Ignored; - } - - let delta = match delta { - mouse::ScrollDelta::Lines { x, y } => { - let is_shift_pressed = state.keyboard_modifiers.shift(); - - // macOS automatically inverts the axes when Shift is pressed - let (x, y) = - if cfg!(target_os = "macos") && is_shift_pressed { - (y, x) - } else { - (x, y) - }; - - let is_vertical = match self.direction { - Direction::Vertical(_) => true, - Direction::Horizontal(_) => false, - Direction::Both { .. } => !is_shift_pressed, - }; - - let movement = if is_vertical { - Vector::new(x, y) - } else { - Vector::new(y, x) - }; - - // TODO: Configurable speed/friction (?) - -movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => -Vector::new(x, y), - }; - - state.scroll( - self.direction.align(delta), - bounds, - content_bounds, - ); - - let has_scrolled = notify_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - let in_transaction = state.last_scrolled.is_some(); - - if has_scrolled || in_transaction { - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Touch(event) - if state.scroll_area_touched_at.is_some() - || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => - { + let mut update = || { + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { match event { - touch::Event::FingerPressed { .. } => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { let Some(cursor_position) = cursor.position() else { return event::Status::Ignored; }; - let delta = Vector::new( - scroll_box_touched_at.x - cursor_position.x, - scroll_box_touched_at.y - cursor_position.y, - ); - - state.scroll( - self.direction.align(delta), + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), bounds, content_bounds, ); - state.scroll_area_touched_at = - Some(cursor_position); + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = + Some(scroller_grabbed_at); - // TODO: bubble up touch movements if not consumed. let _ = notify_scroll( state, &self.on_scroll, @@ -860,25 +620,321 @@ where shell, ); } + + return event::Status::Captured; } _ => {} } - - event::Status::Captured } - Event::Window(window::Event::RedrawRequested(_)) => { - let _ = notify_viewport( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = + Some(scroller_grabbed_at); + + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } + + let content_status = if state.last_scrolled.is_some() + && matches!( + event, + Event::Mouse(mouse::Event::WheelScrolled { .. }) + ) { event::Status::Ignored + } else { + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar + || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + + state.translation( + self.direction, + bounds, + content_bounds, + ), + ) + } + _ => mouse::Cursor::Unavailable, + }; + + let translation = + state.translation(self.direction, bounds, content_bounds); + + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + content, + cursor, + renderer, + clipboard, + shell, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ) + }; + + if matches!( + event, + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch( + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } + ) + ) { + state.scroll_area_touched_at = None; + state.x_scroller_grabbed_at = None; + state.y_scroller_grabbed_at = None; + + return content_status; } - _ => event::Status::Ignored, + + if let event::Status::Captured = content_status { + return event::Status::Captured; + } + + if let Event::Keyboard(keyboard::Event::ModifiersChanged( + modifiers, + )) = event + { + state.keyboard_modifiers = modifiers; + + return event::Status::Ignored; + } + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return event::Status::Ignored; + } + + let delta = match delta { + mouse::ScrollDelta::Lines { x, y } => { + let is_shift_pressed = + state.keyboard_modifiers.shift(); + + // macOS automatically inverts the axes when Shift is pressed + let (x, y) = if cfg!(target_os = "macos") + && is_shift_pressed + { + (y, x) + } else { + (x, y) + }; + + let is_vertical = match self.direction { + Direction::Vertical(_) => true, + Direction::Horizontal(_) => false, + Direction::Both { .. } => !is_shift_pressed, + }; + + let movement = if is_vertical { + Vector::new(x, y) + } else { + Vector::new(y, x) + }; + + // TODO: Configurable speed/friction (?) + -movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => { + -Vector::new(x, y) + } + }; + + state.scroll( + self.direction.align(delta), + bounds, + content_bounds, + ); + + let has_scrolled = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + let in_transaction = state.last_scrolled.is_some(); + + if has_scrolled || in_transaction { + event::Status::Captured + } else { + event::Status::Ignored + } + } + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar + && !mouse_over_x_scrollbar => + { + match event { + touch::Event::FingerPressed { .. } => { + let Some(cursor_position) = cursor.position() + else { + return event::Status::Ignored; + }; + + state.scroll_area_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let Some(cursor_position) = cursor.position() + else { + return event::Status::Ignored; + }; + + let delta = Vector::new( + scroll_box_touched_at.x - cursor_position.x, + scroll_box_touched_at.y - cursor_position.y, + ); + + state.scroll( + self.direction.align(delta), + bounds, + content_bounds, + ); + + state.scroll_area_touched_at = + Some(cursor_position); + + // TODO: bubble up touch movements if not consumed. + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + _ => {} + } + + event::Status::Captured + } + Event::Window(window::Event::RedrawRequested(_)) => { + let _ = notify_viewport( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event::Status::Ignored + } + _ => event::Status::Ignored, + } + }; + + let event_status = update(); + + let status = if state.y_scroller_grabbed_at.is_some() + || state.x_scroller_grabbed_at.is_some() + { + Status::Dragged { + is_horizontal_scrollbar_dragged: state + .x_scroller_grabbed_at + .is_some(), + is_vertical_scrollbar_dragged: state + .y_scroller_grabbed_at + .is_some(), + } + } else if cursor_over_scrollable.is_some() { + Status::Hovered { + is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar, + is_vertical_scrollbar_hovered: mouse_over_y_scrollbar, + } + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(status); } + + if last_offsets != (state.offset_x, state.offset_y) + || self + .last_status + .is_some_and(|last_status| last_status != status) + { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + + event_status } fn draw( @@ -920,27 +976,8 @@ where _ => mouse::Cursor::Unavailable, }; - let status = if state.y_scroller_grabbed_at.is_some() - || state.x_scroller_grabbed_at.is_some() - { - Status::Dragged { - is_horizontal_scrollbar_dragged: state - .x_scroller_grabbed_at - .is_some(), - is_vertical_scrollbar_dragged: state - .y_scroller_grabbed_at - .is_some(), - } - } else if cursor_over_scrollable.is_some() { - Status::Hovered { - is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar, - is_vertical_scrollbar_hovered: mouse_over_y_scrollbar, - } - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Active)); container::draw_background(renderer, &style.container, layout.bounds()); @@ -1323,7 +1360,7 @@ impl operation::Scrollable for State { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] enum Offset { Absolute(f32), Relative(f32), From 752403d70c851ece620c4007710062b158e8dec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Oct 2024 15:40:05 +0200 Subject: [PATCH 401/657] Split `Shell::request_redraw` into two different methods --- core/src/shell.rs | 24 +++++++---- examples/loading_spinners/src/circular.rs | 4 +- examples/loading_spinners/src/linear.rs | 4 +- examples/toast/src/main.rs | 14 +------ widget/src/button.rs | 9 +--- widget/src/checkbox.rs | 12 +++--- widget/src/lazy/component.rs | 19 ++++++++- widget/src/overlay/menu.rs | 14 +++---- widget/src/pick_list.rs | 12 +++--- widget/src/radio.rs | 12 +++--- widget/src/scrollable.rs | 2 +- widget/src/slider.rs | 9 +--- widget/src/text_editor.rs | 6 +-- widget/src/text_input.rs | 50 ++++++++--------------- widget/src/toggler.rs | 12 +++--- 15 files changed, 89 insertions(+), 114 deletions(-) diff --git a/core/src/shell.rs b/core/src/shell.rs index 2952ceff..7a92a2be 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -1,3 +1,4 @@ +use crate::time::Instant; use crate::window; /// A connection to the state of a shell. @@ -35,14 +36,19 @@ impl<'a, Message> Shell<'a, Message> { self.messages.push(message); } - /// Requests a new frame to be drawn. - pub fn request_redraw(&mut self, request: window::RedrawRequest) { + /// Requests a new frame to be drawn as soon as possible. + pub fn request_redraw(&mut self) { + self.redraw_request = Some(window::RedrawRequest::NextFrame); + } + + /// Requests a new frame to be drawn at the given [`Instant`]. + pub fn request_redraw_at(&mut self, at: Instant) { match self.redraw_request { None => { - self.redraw_request = Some(request); + self.redraw_request = Some(window::RedrawRequest::At(at)); } - Some(current) if request < current => { - self.redraw_request = Some(request); + Some(window::RedrawRequest::At(current)) if at < current => { + self.redraw_request = Some(window::RedrawRequest::At(at)); } _ => {} } @@ -95,8 +101,12 @@ impl<'a, Message> Shell<'a, Message> { pub fn merge(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) { self.messages.extend(other.messages.drain(..).map(f)); - if let Some(at) = other.redraw_request { - self.request_redraw(at); + if let Some(new) = other.redraw_request { + self.redraw_request = Some( + self.redraw_request + .map(|current| if current < new { current } else { new }) + .unwrap_or(new), + ); } self.is_layout_invalid = diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 9239f01f..954a777e 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -7,7 +7,7 @@ use iced::event; use iced::mouse; use iced::time::Instant; use iced::widget::canvas; -use iced::window::{self, RedrawRequest}; +use iced::window; use iced::{ Background, Color, Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, @@ -283,7 +283,7 @@ where ); state.cache.clear(); - shell.request_redraw(RedrawRequest::NextFrame); + shell.request_redraw(); } event::Status::Ignored diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 164993c6..81edde75 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -6,7 +6,7 @@ use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; use iced::event; use iced::mouse; use iced::time::Instant; -use iced::window::{self, RedrawRequest}; +use iced::window; use iced::{Background, Color, Element, Event, Length, Rectangle, Size}; use super::easing::{self, Easing}; @@ -192,7 +192,7 @@ where if let Event::Window(window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); - shell.request_redraw(RedrawRequest::NextFrame); + shell.request_redraw(); } event::Status::Ignored diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 8f6a836e..0b46c74e 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -500,8 +500,6 @@ mod toast { shell: &mut Shell<'_, Message>, ) -> event::Status { if let Event::Window(window::Event::RedrawRequested(now)) = &event { - let mut next_redraw: Option = None; - self.instants.iter_mut().enumerate().for_each( |(index, maybe_instant)| { if let Some(instant) = maybe_instant.as_mut() { @@ -512,22 +510,12 @@ mod toast { if remaining == Duration::ZERO { maybe_instant.take(); shell.publish((self.on_close)(index)); - next_redraw = - Some(window::RedrawRequest::NextFrame); } else { - let redraw_at = - window::RedrawRequest::At(*now + remaining); - next_redraw = next_redraw - .map(|redraw| redraw.min(redraw_at)) - .or(Some(redraw_at)); + shell.request_redraw_at(*now + remaining); } } }, ); - - if let Some(redraw) = next_redraw { - shell.request_redraw(redraw); - } } let viewport = layout.bounds(); diff --git a/widget/src/button.rs b/widget/src/button.rs index 5850cea0..46fd0e17 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -366,13 +366,8 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.status = Some(current_status); - } else { - match self.status { - Some(status) if status != current_status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); } update_status diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index e5dea3cc..6c5d7d6b 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -345,13 +345,11 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(current_status); - } else { - match self.last_status { - Some(status) if status != current_status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self + .last_status + .is_some_and(|status| status != current_status) + { + shell.request_redraw(); } event::Status::Ignored diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index c7bc1264..e45c24ac 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -7,6 +7,7 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, @@ -342,7 +343,14 @@ where local_shell.revalidate_layout(|| shell.invalidate_layout()); if let Some(redraw_request) = local_shell.redraw_request() { - shell.request_redraw(redraw_request); + match redraw_request { + window::RedrawRequest::NextFrame => { + shell.request_redraw(); + } + window::RedrawRequest::At(at) => { + shell.request_redraw_at(at); + } + } } if !local_messages.is_empty() { @@ -620,7 +628,14 @@ where local_shell.revalidate_layout(|| shell.invalidate_layout()); if let Some(redraw_request) = local_shell.redraw_request() { - shell.request_redraw(redraw_request); + match redraw_request { + window::RedrawRequest::NextFrame => { + shell.request_redraw(); + } + window::RedrawRequest::At(at) => { + shell.request_redraw_at(at); + } + } } if !local_messages.is_empty() { diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index e79bd3da..c1a0a5d8 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -435,9 +435,7 @@ where .publish(on_option_hovered(option.clone())); } - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } } @@ -472,14 +470,12 @@ where let state = tree.state.downcast_mut::(); - if state.is_hovered.is_some_and(|is_hovered| { - is_hovered != cursor.is_over(layout.bounds()) - }) { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - if let Event::Window(window::Event::RedrawRequested(_now)) = event { state.is_hovered = Some(cursor.is_over(layout.bounds())); + } else if state.is_hovered.is_some_and(|is_hovered| { + is_hovered != cursor.is_over(layout.bounds()) + }) { + shell.request_redraw(); } event::Status::Ignored diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index ec1c054f..9c9ba9e9 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -535,13 +535,11 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(status); - } else { - match self.last_status { - Some(last_status) if last_status != status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self + .last_status + .is_some_and(|last_status| last_status != status) + { + shell.request_redraw(); } event_status diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 714d4fb5..ed821532 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -360,13 +360,11 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(current_status); - } else { - match self.last_status { - Some(status) if status != current_status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self + .last_status + .is_some_and(|last_status| last_status != current_status) + { + shell.request_redraw(); } event::Status::Ignored diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index c4350547..abad6ea6 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -931,7 +931,7 @@ where .last_status .is_some_and(|last_status| last_status != status) { - shell.request_redraw(window::RedrawRequest::NextFrame); + shell.request_redraw(); } event_status diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 25f0d85f..dbdb5f07 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -432,13 +432,8 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.status = Some(current_status); - } else { - match self.status { - Some(status) if status != current_status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); } update_status diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a298252a..3bb45494 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -624,7 +624,7 @@ where focus.is_window_focused = true; focus.updated_at = Instant::now(); - shell.request_redraw(window::RedrawRequest::NextFrame); + shell.request_redraw(); } } Event::Window(window::Event::RedrawRequested(now)) => { @@ -637,11 +637,11 @@ where - (now - focus.updated_at).as_millis() % Focus::CURSOR_BLINK_INTERVAL_MILLIS; - shell.request_redraw(window::RedrawRequest::At( + shell.request_redraw_at( now + Duration::from_millis( millis_until_redraw as u64, ), - )); + ); } } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index c18009a2..8fa7889f 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -751,7 +751,7 @@ where state.last_click = Some(click); if cursor_before != state.cursor { - shell.request_redraw(window::RedrawRequest::NextFrame); + shell.request_redraw(); } return event::Status::Captured; @@ -806,7 +806,7 @@ where } if selection_before != state.cursor.selection(&value) { - shell.request_redraw(window::RedrawRequest::NextFrame); + shell.request_redraw(); } return event::Status::Captured; @@ -914,9 +914,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1037,9 +1035,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1059,9 +1055,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1083,9 +1077,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1107,9 +1099,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1136,9 +1126,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1165,9 +1153,7 @@ where if cursor_before != state.cursor { focus.updated_at = Instant::now(); - shell.request_redraw( - window::RedrawRequest::NextFrame, - ); + shell.request_redraw(); } return event::Status::Captured; @@ -1218,7 +1204,7 @@ where focus.is_window_focused = true; focus.updated_at = Instant::now(); - shell.request_redraw(window::RedrawRequest::NextFrame); + shell.request_redraw(); } } Event::Window(window::Event::RedrawRequested(now)) => { @@ -1237,11 +1223,11 @@ where - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - shell.request_redraw(window::RedrawRequest::At( + shell.request_redraw_at( *now + Duration::from_millis( millis_until_redraw as u64, ), - )); + ); } } } @@ -1265,13 +1251,11 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(status); - } else { - match self.last_status { - Some(last_status) if status != last_status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self + .last_status + .is_some_and(|last_status| status != last_status) + { + shell.request_redraw(); } event::Status::Ignored diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 13244e34..2553a7e4 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -352,13 +352,11 @@ where if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(current_status); - } else { - match self.last_status { - Some(status) if status != current_status => { - shell.request_redraw(window::RedrawRequest::NextFrame); - } - _ => {} - } + } else if self + .last_status + .is_some_and(|status| status != current_status) + { + shell.request_redraw(); } event_status From dcc184b01b753dbecb500205391f6eaaa21c8683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Oct 2024 19:28:18 +0200 Subject: [PATCH 402/657] Replace `event::Status` in `Widget::on_event` with `Shell::capture_event` --- core/src/element.rs | 15 +-- core/src/overlay.rs | 6 +- core/src/overlay/element.rs | 13 +- core/src/overlay/group.rs | 27 ++-- core/src/shell.rs | 23 ++++ core/src/widget.rs | 6 +- examples/loading_spinners/src/circular.rs | 5 +- examples/loading_spinners/src/linear.rs | 5 +- examples/toast/src/main.rs | 53 ++++---- runtime/src/overlay/nested.rs | 74 +++++----- runtime/src/user_interface.rs | 10 +- widget/src/button.rs | 89 ++++++------ widget/src/canvas.rs | 8 +- widget/src/checkbox.rs | 11 +- widget/src/column.rs | 35 +++-- widget/src/combo_box.rs | 24 ++-- widget/src/container.rs | 7 +- widget/src/helpers.rs | 38 ++---- widget/src/image/viewer.rs | 30 ++--- widget/src/keyed/column.rs | 35 +++-- widget/src/lazy.rs | 18 ++- widget/src/lazy/component.rs | 43 +++--- widget/src/lazy/responsive.rs | 31 ++--- widget/src/mouse_area.rs | 156 +++++++++------------- widget/src/overlay/menu.rs | 17 +-- widget/src/pane_grid.rs | 47 +++---- widget/src/pane_grid/content.rs | 20 +-- widget/src/pane_grid/title_bar.rs | 30 ++--- widget/src/pick_list.rs | 25 ++-- widget/src/radio.rs | 12 +- widget/src/row.rs | 37 +++-- widget/src/scrollable.rs | 62 ++++----- widget/src/shader.rs | 16 +-- widget/src/shader/event.rs | 2 - widget/src/shader/program.rs | 4 +- widget/src/slider.rs | 23 ++-- widget/src/stack.rs | 53 ++++---- widget/src/text/rich.rs | 18 ++- widget/src/text_editor.rs | 13 +- widget/src/text_input.rs | 66 +++++---- widget/src/themer.rs | 16 +-- widget/src/toggler.rs | 18 +-- widget/src/tooltip.rs | 9 +- widget/src/vertical_slider.rs | 19 ++- 44 files changed, 560 insertions(+), 709 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index 6ebb8a15..8276b70c 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,4 +1,3 @@ -use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; @@ -6,8 +5,8 @@ use crate::renderer; use crate::widget; use crate::widget::tree::{self, Tree}; use crate::{ - Border, Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector, - Widget, + Border, Clipboard, Color, Event, Layout, Length, Rectangle, Shell, Size, + Vector, Widget, }; use std::borrow::Borrow; @@ -319,11 +318,11 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, B>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); - let status = self.widget.on_event( + self.widget.on_event( tree, event, layout, @@ -335,8 +334,6 @@ where ); shell.merge(local_shell, &self.mapper); - - status } fn draw( @@ -457,10 +454,10 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.element.widget.on_event( state, event, layout, cursor, renderer, clipboard, shell, viewport, - ) + ); } fn draw( diff --git a/core/src/overlay.rs b/core/src/overlay.rs index f09de831..e063bb94 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -5,13 +5,12 @@ mod group; pub use element::Element; pub use group::Group; -use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; use crate::widget; use crate::widget::Tree; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; +use crate::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size, Vector}; /// An interactive component that can be displayed on top of other widgets. pub trait Overlay @@ -65,8 +64,7 @@ where _renderer: &Renderer, _clipboard: &mut dyn Clipboard, _shell: &mut Shell<'_, Message>, - ) -> event::Status { - event::Status::Ignored + ) { } /// Returns the current [`mouse::Interaction`] of the [`Overlay`]. diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 32e987a3..4a242213 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -1,11 +1,10 @@ pub use crate::Overlay; -use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; use crate::widget; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; +use crate::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size}; /// A generic [`Overlay`]. #[allow(missing_debug_implementations)] @@ -58,9 +57,9 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { self.overlay - .on_event(event, layout, cursor, renderer, clipboard, shell) + .on_event(event, layout, cursor, renderer, clipboard, shell); } /// Returns the current [`mouse::Interaction`] of the [`Element`]. @@ -157,11 +156,11 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, B>, - ) -> event::Status { + ) { let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); - let event_status = self.content.on_event( + self.content.on_event( event, layout, cursor, @@ -171,8 +170,6 @@ where ); shell.merge(local_shell, self.mapper); - - event_status } fn mouse_interaction( diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 6541d311..11ebd579 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -1,4 +1,3 @@ -use crate::event; use crate::layout; use crate::mouse; use crate::overlay; @@ -81,21 +80,17 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(layout.children()) - .map(|(child, layout)| { - child.on_event( - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + ) { + for (child, layout) in self.children.iter_mut().zip(layout.children()) { + child.on_event( + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + ); + } } fn draw( diff --git a/core/src/shell.rs b/core/src/shell.rs index 7a92a2be..12ebbaa8 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -1,3 +1,4 @@ +use crate::event; use crate::time::Instant; use crate::window; @@ -10,6 +11,7 @@ use crate::window; #[derive(Debug)] pub struct Shell<'a, Message> { messages: &'a mut Vec, + event_status: event::Status, redraw_request: Option, is_layout_invalid: bool, are_widgets_invalid: bool, @@ -20,6 +22,7 @@ impl<'a, Message> Shell<'a, Message> { pub fn new(messages: &'a mut Vec) -> Self { Self { messages, + event_status: event::Status::Ignored, redraw_request: None, is_layout_invalid: false, are_widgets_invalid: false, @@ -36,6 +39,24 @@ impl<'a, Message> Shell<'a, Message> { self.messages.push(message); } + /// Marks the current event as captured. Prevents "event bubbling". + /// + /// A widget should capture an event when no ancestor should + /// handle it. + pub fn capture_event(&mut self) { + self.event_status = event::Status::Captured; + } + + /// Returns the current [`event::Status`] of the [`Shell`]. + pub fn event_status(&self) -> event::Status { + self.event_status + } + + /// Returns whether the current event has been captured. + pub fn is_event_captured(&self) -> bool { + self.event_status == event::Status::Captured + } + /// Requests a new frame to be drawn as soon as possible. pub fn request_redraw(&mut self) { self.redraw_request = Some(window::RedrawRequest::NextFrame); @@ -114,5 +135,7 @@ impl<'a, Message> Shell<'a, Message> { self.are_widgets_invalid = self.are_widgets_invalid || other.are_widgets_invalid; + + self.event_status = self.event_status.merge(other.event_status); } } diff --git a/core/src/widget.rs b/core/src/widget.rs index 9cfff83d..bddf99cc 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -10,12 +10,11 @@ pub use operation::Operation; pub use text::Text; pub use tree::Tree; -use crate::event::{self, Event}; use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector}; +use crate::{Clipboard, Event, Length, Rectangle, Shell, Size, Vector}; /// A component that displays information and allows interaction. /// @@ -122,8 +121,7 @@ where _clipboard: &mut dyn Clipboard, _shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { - event::Status::Ignored + ) { } /// Returns the current [`mouse::Interaction`] of the [`Widget`]. diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 954a777e..e6b59cae 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -3,7 +3,6 @@ use iced::advanced::layout; use iced::advanced::renderer; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::event; use iced::mouse; use iced::time::Instant; use iced::widget::canvas; @@ -272,7 +271,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { @@ -285,8 +284,6 @@ where state.cache.clear(); shell.request_redraw(); } - - event::Status::Ignored } fn draw( diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 81edde75..34576261 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -3,7 +3,6 @@ use iced::advanced::layout; use iced::advanced::renderer::{self, Quad}; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; -use iced::event; use iced::mouse; use iced::time::Instant; use iced::window; @@ -186,7 +185,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { @@ -194,8 +193,6 @@ where shell.request_redraw(); } - - event::Status::Ignored } fn draw( diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 0b46c74e..079b96b4 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -169,7 +169,6 @@ mod toast { use iced::advanced::renderer; use iced::advanced::widget::{self, Operation, Tree}; use iced::advanced::{Clipboard, Shell, Widget}; - use iced::event::{self, Event}; use iced::mouse; use iced::theme; use iced::widget::{ @@ -177,8 +176,8 @@ mod toast { }; use iced::window; use iced::{ - Alignment, Center, Element, Fill, Length, Point, Rectangle, Renderer, - Size, Theme, Vector, + Alignment, Center, Element, Event, Fill, Length, Point, Rectangle, + Renderer, Size, Theme, Vector, }; pub const DEFAULT_TIMEOUT: u64 = 5; @@ -369,7 +368,7 @@ mod toast { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.content.as_widget_mut().on_event( &mut state.children[0], event, @@ -379,7 +378,7 @@ mod toast { clipboard, shell, viewport, - ) + ); } fn draw( @@ -498,7 +497,7 @@ mod toast { renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { if let Event::Window(window::Event::RedrawRequested(now)) = &event { self.instants.iter_mut().enumerate().for_each( |(index, maybe_instant)| { @@ -520,35 +519,33 @@ mod toast { let viewport = layout.bounds(); - self.toasts + for (((child, state), layout), instant) in self + .toasts .iter_mut() .zip(self.state.iter_mut()) .zip(layout.children()) .zip(self.instants.iter_mut()) - .map(|(((child, state), layout), instant)| { - let mut local_messages = vec![]; - let mut local_shell = Shell::new(&mut local_messages); + { + let mut local_messages = vec![]; + let mut local_shell = Shell::new(&mut local_messages); - let status = child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - &mut local_shell, - &viewport, - ); + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + &viewport, + ); - if !local_shell.is_empty() { - instant.take(); - } + if !local_shell.is_empty() { + instant.take(); + } - shell.merge(local_shell, std::convert::identity); - - status - }) - .fold(event::Status::Ignored, event::Status::merge) + shell.merge(local_shell, std::convert::identity); + } } fn draw( diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index da3e6929..45f6b220 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -166,7 +166,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { fn recurse( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, @@ -175,31 +175,30 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> (event::Status, bool) + ) -> bool where Renderer: renderer::Renderer, { let mut layouts = layout.children(); if let Some(layout) = layouts.next() { - let (nested_status, nested_is_over) = - if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) - { - recurse( - &mut nested, - nested_layout, - event.clone(), - cursor, - renderer, - clipboard, - shell, - ) - } else { - (event::Status::Ignored, false) - }; + let nested_is_over = if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + event.clone(), + cursor, + renderer, + clipboard, + shell, + ) + } else { + false + }; - if matches!(nested_status, event::Status::Ignored) { + if shell.event_status() == event::Status::Ignored { let is_over = nested_is_over || cursor .position() @@ -212,30 +211,29 @@ where }) .unwrap_or_default(); - ( - element.on_event( - event, - layout, - if nested_is_over { - mouse::Cursor::Unavailable - } else { - cursor - }, - renderer, - clipboard, - shell, - ), - is_over, - ) + element.on_event( + event, + layout, + if nested_is_over { + mouse::Cursor::Unavailable + } else { + cursor + }, + renderer, + clipboard, + shell, + ); + + is_over } else { - (nested_status, nested_is_over) + nested_is_over } } else { - (event::Status::Ignored, false) + false } } - let (status, _) = recurse( + let _ = recurse( &mut self.overlay, layout, event, @@ -244,8 +242,6 @@ where clipboard, shell, ); - - status } /// Returns the current [`mouse::Interaction`] of the [`Nested`] overlay. diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 8dfc97a7..cae17bcc 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -210,7 +210,7 @@ where for event in events.iter().cloned() { let mut shell = Shell::new(messages); - let event_status = overlay.on_event( + overlay.on_event( event, Layout::new(&layout), cursor, @@ -219,7 +219,7 @@ where &mut shell, ); - event_statuses.push(event_status); + event_statuses.push(shell.event_status()); match (redraw_request, shell.redraw_request()) { (None, Some(at)) => { @@ -308,7 +308,7 @@ where let mut shell = Shell::new(messages); - let event_status = self.root.as_widget_mut().on_event( + self.root.as_widget_mut().on_event( &mut self.state, event, Layout::new(&self.base), @@ -319,7 +319,7 @@ where &viewport, ); - if matches!(event_status, event::Status::Captured) { + if shell.event_status() == event::Status::Captured { self.overlay = None; } @@ -347,7 +347,7 @@ where outdated = true; } - event_status.merge(overlay_status) + shell.event_status().merge(overlay_status) }) .collect(); diff --git a/widget/src/button.rs b/widget/src/button.rs index 46fd0e17..6be17f59 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -283,8 +283,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().on_event( &mut tree.children[0], event.clone(), layout.children().next().unwrap(), @@ -293,62 +293,53 @@ where clipboard, shell, viewport, - ) { - return event::Status::Captured; + ); + + if shell.event_status() == event::Status::Captured { + return; } - let mut update = || { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if self.on_press.is_some() { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if self.on_press.is_some() { + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + let state = tree.state.downcast_mut::(); + + state.is_pressed = true; + + shell.capture_event(); + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = self.on_press.as_ref().map(OnPress::get) + { + let state = tree.state.downcast_mut::(); + + if state.is_pressed { + state.is_pressed = false; + let bounds = layout.bounds(); if cursor.is_over(bounds) { - let state = tree.state.downcast_mut::(); - - state.is_pressed = true; - - return event::Status::Captured; + shell.publish(on_press); } + + shell.capture_event(); } } - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = - self.on_press.as_ref().map(OnPress::get) - { - let state = tree.state.downcast_mut::(); - - if state.is_pressed { - state.is_pressed = false; - - let bounds = layout.bounds(); - - if cursor.is_over(bounds) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = tree.state.downcast_mut::(); - - state.is_pressed = false; - } - _ => {} } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = tree.state.downcast_mut::(); - event::Status::Ignored - }; - - let update_status = update(); + state.is_pressed = false; + } + _ => {} + } let current_status = if self.on_press.is_none() { Status::Disabled @@ -369,8 +360,6 @@ where } else if self.status.is_some_and(|status| status != current_status) { shell.request_redraw(); } - - update_status } fn draw( diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 9fbccf82..a9c65bb6 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -223,7 +223,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); let canvas_event = match event { @@ -245,10 +245,10 @@ where shell.publish(message); } - return event_status; + if event_status == event::Status::Captured { + shell.capture_event(); + } } - - event::Status::Ignored } fn mouse_interaction( diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 6c5d7d6b..9b5f3602 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -31,7 +31,6 @@ //! ``` //! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) use crate::core::alignment; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -42,8 +41,8 @@ use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Rectangle, Shell, Size, Theme, Widget, + Background, Border, Clipboard, Color, Element, Event, Layout, Length, + Pixels, Rectangle, Shell, Size, Theme, Widget, }; /// A box that can be checked. @@ -313,7 +312,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -322,7 +321,7 @@ where if mouse_over { if let Some(on_toggle) = &self.on_toggle { shell.publish((on_toggle)(!self.is_checked)); - return event::Status::Captured; + shell.capture_event(); } } } @@ -351,8 +350,6 @@ where { shell.request_redraw(); } - - event::Status::Ignored } fn mouse_interaction( diff --git a/widget/src/column.rs b/widget/src/column.rs index 213f68fc..3fdc17f8 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,14 +1,13 @@ //! Distribute content vertically. use crate::core::alignment::{self, Alignment}; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, - Size, Vector, Widget, + Clipboard, Element, Event, Layout, Length, Padding, Pixels, Rectangle, + Shell, Size, Vector, Widget, }; /// A container that distributes its contents vertically. @@ -268,24 +267,24 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index e300f1d0..1122861f 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -54,7 +54,7 @@ //! } //! } //! ``` -use crate::core::event::{self, Event}; +use crate::core::event; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; @@ -65,7 +65,8 @@ use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector, + Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme, + Vector, }; use crate::overlay::menu; use crate::text::LineHeight; @@ -519,7 +520,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let menu = tree.state.downcast_mut::>(); let started_focused = { @@ -538,7 +539,7 @@ where let mut local_shell = Shell::new(&mut local_messages); // Provide it to the widget - let mut event_status = self.text_input.on_event( + self.text_input.on_event( &mut tree.children[0], event.clone(), layout, @@ -549,13 +550,16 @@ where viewport, ); + if local_shell.event_status() == event::Status::Captured { + shell.capture_event(); + } + // Then finally react to them here for message in local_messages { let TextInputEvent::TextChanged(new_value) = message; if let Some(on_input) = &self.on_input { shell.publish((on_input)(new_value.clone())); - published_message_to_shell = true; } // Couple the filtered options with the `ComboBox` @@ -619,7 +623,7 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); } (key::Named::ArrowUp, _) | (key::Named::Tab, true) => { @@ -656,7 +660,7 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); } (key::Named::ArrowDown, _) | (key::Named::Tab, false) @@ -703,7 +707,7 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); } _ => {} } @@ -724,7 +728,7 @@ where published_message_to_shell = true; // Unfocus the input - let _ = self.text_input.on_event( + self.text_input.on_event( &mut tree.children[0], Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, @@ -761,8 +765,6 @@ where } } } - - event_status } fn mouse_interaction( diff --git a/widget/src/container.rs b/widget/src/container.rs index f4993ac9..f96c495c 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -21,7 +21,6 @@ //! ``` use crate::core::alignment::{self, Alignment}; use crate::core::border::{self, Border}; -use crate::core::event::{self, Event}; use crate::core::gradient::{self, Gradient}; use crate::core::layout; use crate::core::mouse; @@ -30,7 +29,7 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - self, color, Background, Clipboard, Color, Element, Layout, Length, + self, color, Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; @@ -308,7 +307,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.content.as_widget_mut().on_event( tree, event, @@ -318,7 +317,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 52290a54..13d69e1f 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -363,12 +363,11 @@ where Theme: 'a, Renderer: core::Renderer + 'a, { - use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; - use crate::core::{Rectangle, Shell, Size}; + use crate::core::{Event, Rectangle, Shell, Size}; struct Opaque<'a, Message, Theme, Renderer> { content: Element<'a, Message, Theme, Renderer>, @@ -449,25 +448,19 @@ where clipboard: &mut dyn core::Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let is_mouse_press = matches!( event, core::Event::Mouse(mouse::Event::ButtonPressed(_)) ); - if let core::event::Status::Captured = - self.content.as_widget_mut().on_event( - state, event, layout, cursor, renderer, clipboard, shell, - viewport, - ) - { - return event::Status::Captured; - } + self.content.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ); if is_mouse_press && cursor.is_over(layout.bounds()) { - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } @@ -530,12 +523,11 @@ where Theme: 'a, Renderer: core::Renderer + 'a, { - use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; - use crate::core::{Rectangle, Shell, Size}; + use crate::core::{Event, Rectangle, Shell, Size}; struct Hover<'a, Message, Theme, Renderer> { base: Element<'a, Message, Theme, Renderer>, @@ -658,7 +650,7 @@ where clipboard: &mut dyn core::Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut children = layout.children().zip(&mut tree.children); let (base_layout, base_tree) = children.next().unwrap(); let (top_layout, top_tree) = children.next().unwrap(); @@ -680,7 +672,7 @@ where }; } - let top_status = if matches!( + if matches!( event, Event::Mouse( mouse::Event::CursorMoved { .. } @@ -699,13 +691,11 @@ where clipboard, shell, viewport, - ) - } else { - event::Status::Ignored + ); }; - if top_status == event::Status::Captured { - return top_status; + if shell.is_event_captured() { + return; } self.base.as_widget_mut().on_event( @@ -717,7 +707,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index b1aad22c..5787200b 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,13 +1,12 @@ //! Zoom and pan on an image. -use crate::core::event::{self, Event}; use crate::core::image::{self, FilterMethod}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, ContentFit, Element, Image, Layout, Length, Pixels, Point, - Radians, Rectangle, Shell, Size, Vector, Widget, + Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels, + Point, Radians, Rectangle, Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -157,15 +156,15 @@ where cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, + shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { let Some(cursor_position) = cursor.position_over(bounds) else { - return event::Status::Ignored; + return; }; match delta { @@ -216,29 +215,25 @@ where } } - event::Status::Captured + shell.capture_event(); } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let Some(cursor_position) = cursor.position_over(bounds) else { - return event::Status::Ignored; + return; }; let state = tree.state.downcast_mut::(); state.cursor_grabbed_at = Some(cursor_position); state.starting_offset = state.current_offset; - - event::Status::Captured + shell.capture_event(); } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { let state = tree.state.downcast_mut::(); if state.cursor_grabbed_at.is_some() { state.cursor_grabbed_at = None; - - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::CursorMoved { position }) => { @@ -278,13 +273,10 @@ where }; state.current_offset = Vector::new(x, y); - - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } - _ => event::Status::Ignored, + _ => {} } } diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 5852ede1..1172785a 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -1,5 +1,4 @@ //! Keyed columns distribute content vertically while keeping continuity. -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -7,8 +6,8 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Size, Vector, Widget, + Alignment, Clipboard, Element, Event, Layout, Length, Padding, Pixels, + Rectangle, Shell, Size, Vector, Widget, }; /// A container that distributes its contents vertically while keeping continuity. @@ -308,24 +307,24 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 232f254c..4a9a6154 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -10,7 +10,6 @@ pub use responsive::Responsive; mod cache; -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -19,7 +18,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::Element; use crate::core::{ - self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Event, Length, Point, Rectangle, Shell, Size, Vector, }; use crate::runtime::overlay::Nested; @@ -206,7 +205,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.with_element_mut(|element| { element.as_widget_mut().on_event( &mut tree.children[0], @@ -217,8 +216,8 @@ where clipboard, shell, viewport, - ) - }) + ); + }); } fn mouse_interaction( @@ -395,11 +394,10 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event(event, layout, cursor, renderer, clipboard, shell) - }) - .unwrap_or(event::Status::Ignored) + ) { + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.on_event(event, layout, cursor, renderer, clipboard, shell); + }); } fn is_over( diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index e45c24ac..062e6f35 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -1,6 +1,5 @@ //! Build and reuse custom widgets using The Elm Architecture. #![allow(deprecated)] -use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -322,12 +321,12 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); let t = tree.state.downcast_mut::>>>(); - let event_status = self.with_element_mut(|element| { + self.with_element_mut(|element| { element.as_widget_mut().on_event( &mut t.borrow_mut().as_mut().unwrap().children[0], event, @@ -337,9 +336,13 @@ where clipboard, &mut local_shell, viewport, - ) + ); }); + if local_shell.is_event_captured() { + shell.capture_event(); + } + local_shell.revalidate_layout(|| shell.invalidate_layout()); if let Some(redraw_request) = local_shell.redraw_request() { @@ -377,8 +380,6 @@ where shell.invalidate_layout(); } - - event_status } fn operate( @@ -608,22 +609,24 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); - let event_status = self - .with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor, - renderer, - clipboard, - &mut local_shell, - ) - }) - .unwrap_or(event::Status::Ignored); + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + ); + }); + + if local_shell.is_event_captured() { + shell.capture_event(); + } local_shell.revalidate_layout(|| shell.invalidate_layout()); @@ -673,8 +676,6 @@ where shell.invalidate_layout(); } - - event_status } fn is_over( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index a6c40ab0..c17798a6 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -1,4 +1,3 @@ -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -6,8 +5,8 @@ use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, - Widget, + self, Clipboard, Element, Event, Length, Point, Rectangle, Shell, Size, + Vector, Widget, }; use crate::horizontal_space; use crate::runtime::overlay::Nested; @@ -193,14 +192,14 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); let mut content = self.content.borrow_mut(); let mut local_messages = vec![]; let mut local_shell = Shell::new(&mut local_messages); - let status = content.resolve( + content.resolve( &mut state.tree.borrow_mut(), renderer, layout, @@ -215,7 +214,7 @@ where clipboard, &mut local_shell, viewport, - ) + ); }, ); @@ -224,8 +223,6 @@ where } shell.merge(local_shell, std::convert::identity); - - status } fn draw( @@ -425,28 +422,20 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let mut is_layout_invalid = false; - let event_status = self - .with_overlay_mut_maybe(|overlay| { - let event_status = overlay.on_event( - event, layout, cursor, renderer, clipboard, shell, - ); + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.on_event(event, layout, cursor, renderer, clipboard, shell); - is_layout_invalid = shell.is_layout_invalid(); - - event_status - }) - .unwrap_or(event::Status::Ignored); + is_layout_invalid = shell.is_layout_invalid(); + }); if is_layout_invalid { self.with_overlay_mut(|(_overlay, layout)| { **layout = None; }); } - - event_status } fn is_over( diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index c5a37ae3..50188abd 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,5 +1,4 @@ //! A container for capturing mouse events. -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -7,8 +6,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, - Widget, + Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, + Vector, Widget, }; /// Emit messages on mouse events. @@ -226,8 +225,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().on_event( &mut tree.children[0], event.clone(), layout, @@ -236,11 +235,13 @@ where clipboard, shell, viewport, - ) { - return event::Status::Captured; + ); + + if shell.is_event_captured() { + return; } - update(self, tree, event, layout, cursor, shell) + update(self, tree, event, layout, cursor, shell); } fn mouse_interaction( @@ -329,7 +330,7 @@ fn update( layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, -) -> event::Status { +) { let state: &mut State = tree.state.downcast_mut(); let cursor_position = cursor.position(); @@ -363,104 +364,71 @@ fn update( } if !cursor.is_over(layout.bounds()) { - return event::Status::Ignored; + return; } - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { - let mut captured = false; + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(message) = widget.on_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } - if let Some(message) = widget.on_press.as_ref() { - captured = true; - shell.publish(message.clone()); - } + if let Some(position) = cursor_position { + if let Some(message) = widget.on_double_click.as_ref() { + let new_click = mouse::Click::new( + position, + mouse::Button::Left, + state.previous_click, + ); - if let Some(position) = cursor_position { - if let Some(message) = widget.on_double_click.as_ref() { - let new_click = mouse::Click::new( - position, - mouse::Button::Left, - state.previous_click, - ); + if matches!(new_click.kind(), mouse::click::Kind::Double) { + shell.publish(message.clone()); + } - if matches!(new_click.kind(), mouse::click::Kind::Double) { - shell.publish(message.clone()); + state.previous_click = Some(new_click); + + // Even if this is not a double click, but the press is nevertheless + // processed by us and should not be popup to parent widgets. + shell.capture_event(); } - - state.previous_click = Some(new_click); - - // Even if this is not a double click, but the press is nevertheless - // processed by us and should not be popup to parent widgets. - captured = true; } } - - if captured { - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(message) = widget.on_release.as_ref() { + shell.publish(message.clone()); + } } - } - - if let Some(message) = widget.on_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + if let Some(message) = widget.on_right_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } } - } - - if let Some(message) = widget.on_right_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = - event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => { + if let Some(message) = widget.on_right_release.as_ref() { + shell.publish(message.clone()); + } } - } - - if let Some(message) = widget.on_right_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Right, - )) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => { + if let Some(message) = widget.on_middle_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } } - } - - if let Some(message) = widget.on_middle_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Middle, - )) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => { + if let Some(message) = widget.on_middle_release.as_ref() { + shell.publish(message.clone()); + } } - } - - if let Some(message) = widget.on_middle_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Middle, - )) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(on_scroll) = widget.on_scroll.as_ref() { + shell.publish(on_scroll(delta)); + shell.capture_event(); + } } + _ => {} } - - if let Some(on_scroll) = widget.on_scroll.as_ref() { - if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { - shell.publish(on_scroll(delta)); - - return event::Status::Captured; - } - } - - event::Status::Ignored } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index c1a0a5d8..78ee3da6 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -1,7 +1,6 @@ //! Build and show dropdown menus. use crate::core::alignment; use crate::core::border::{self, Border}; -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -11,8 +10,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, - Size, Theme, Vector, + Background, Clipboard, Color, Event, Length, Padding, Pixels, Point, + Rectangle, Size, Theme, Vector, }; use crate::core::{Element, Shell, Widget}; use crate::scrollable::{self, Scrollable}; @@ -271,13 +270,13 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); self.list.on_event( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, - ) + ); } fn mouse_interaction( @@ -397,14 +396,14 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { if cursor.is_over(layout.bounds()) { if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { shell.publish((self.on_selected)(option.clone())); - return event::Status::Captured; + shell.capture_event(); } } } @@ -460,7 +459,7 @@ where if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { shell.publish((self.on_selected)(option.clone())); - return event::Status::Captured; + shell.capture_event(); } } } @@ -477,8 +476,6 @@ where }) { shell.request_redraw(); } - - event::Status::Ignored } fn mouse_interaction( diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index b4ed4b64..29b7ac87 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -79,7 +79,6 @@ pub use state::State; pub use title_bar::TitleBar; use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay::{self, Group}; @@ -88,7 +87,7 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, + self, Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; @@ -433,9 +432,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let mut event_status = event::Status::Ignored; - + ) { let Memory { action, .. } = tree.state.downcast_mut(); let node = self.internal.layout(); @@ -451,7 +448,7 @@ where let bounds = layout.bounds(); if let Some(cursor_position) = cursor.position_over(bounds) { - event_status = event::Status::Captured; + shell.capture_event(); match &self.on_resize { Some((leeway, _)) => { @@ -556,9 +553,9 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); } else if action.picked_split().is_some() { - event_status = event::Status::Captured; + shell.capture_event(); } *action = state::Action::Idle; @@ -600,7 +597,7 @@ where ratio, })); - event_status = event::Status::Captured; + shell.capture_event(); } } } @@ -611,7 +608,8 @@ where let picked_pane = action.picked_pane().map(|(pane, _)| pane); - self.panes + for (((pane, content), tree), layout) in self + .panes .iter() .copied() .zip(&mut self.contents) @@ -622,22 +620,21 @@ where .maximized() .map_or(true, |maximized| *pane == maximized) }) - .map(|(((pane, content), tree), layout)| { - let is_picked = picked_pane == Some(pane); + { + let is_picked = picked_pane == Some(pane); - content.on_event( - tree, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - is_picked, - ) - }) - .fold(event_status, event::Status::merge) + content.on_event( + tree, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + is_picked, + ); + } } fn mouse_interaction( diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index ec0676b1..81a5cc1e 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,12 +1,12 @@ use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size, + Vector, }; use crate::pane_grid::{Draggable, TitleBar}; @@ -250,13 +250,11 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, is_picked: bool, - ) -> event::Status { - let mut event_status = event::Status::Ignored; - + ) { let body_layout = if let Some(title_bar) = &mut self.title_bar { let mut children = layout.children(); - event_status = title_bar.on_event( + title_bar.on_event( &mut tree.children[1], event.clone(), children.next().unwrap(), @@ -272,9 +270,7 @@ where layout }; - let body_status = if is_picked { - event::Status::Ignored - } else { + if !is_picked { self.body.as_widget_mut().on_event( &mut tree.children[0], event, @@ -284,10 +280,8 @@ where clipboard, shell, viewport, - ) - }; - - event_status.merge(body_status) + ); + } } pub(crate) fn mouse_interaction( diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 5002b4f7..ec1dc302 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -1,13 +1,12 @@ use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, - Vector, + self, Clipboard, Element, Event, Layout, Padding, Point, Rectangle, Shell, + Size, Vector, }; use crate::pane_grid::controls::Controls; @@ -438,7 +437,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut children = layout.children(); let padded = children.next().unwrap(); @@ -446,8 +445,9 @@ where let title_layout = children.next().unwrap(); let mut show_title = true; - let control_status = if let Some(controls) = &mut self.controls { + if let Some(controls) = &mut self.controls { let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { @@ -463,7 +463,7 @@ where clipboard, shell, viewport, - ) + ); } else { show_title = false; @@ -476,7 +476,7 @@ where clipboard, shell, viewport, - ) + ); } } else { controls.full.as_widget_mut().on_event( @@ -488,13 +488,11 @@ where clipboard, shell, viewport, - ) + ); } - } else { - event::Status::Ignored - }; + } - let title_status = if show_title { + if show_title { self.content.as_widget_mut().on_event( &mut tree.children[0], event, @@ -504,12 +502,8 @@ where clipboard, shell, viewport, - ) - } else { - event::Status::Ignored - }; - - control_status.merge(title_status) + ); + } } pub(crate) fn mouse_interaction( diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 9c9ba9e9..9eb43e72 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -61,7 +61,6 @@ //! } //! ``` use crate::core::alignment; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; @@ -73,8 +72,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, - Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + Background, Border, Clipboard, Color, Element, Event, Layout, Length, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::overlay::menu::{self, Menu}; @@ -438,10 +437,10 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::>(); - let event_status = match event { + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if state.is_open { @@ -453,7 +452,7 @@ where shell.publish(on_close.clone()); } - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { let selected = self.selected.as_ref().map(Borrow::borrow); @@ -468,9 +467,7 @@ where shell.publish(on_open.clone()); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -512,17 +509,13 @@ where shell.publish((self.on_select)(next_option.clone())); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} }; let status = if state.is_open { @@ -541,8 +534,6 @@ where { shell.request_redraw(); } - - event_status } fn mouse_interaction( diff --git a/widget/src/radio.rs b/widget/src/radio.rs index ed821532..70e1c423 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -58,7 +58,6 @@ //! ``` use crate::core::alignment; use crate::core::border::{self, Border}; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -68,8 +67,8 @@ use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle, - Shell, Size, Theme, Widget, + Background, Clipboard, Color, Element, Event, Layout, Length, Pixels, + Rectangle, Shell, Size, Theme, Widget, }; /// A circular button representing a choice. @@ -334,14 +333,13 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - - return event::Status::Captured; + shell.capture_event(); } } _ => {} @@ -366,8 +364,6 @@ where { shell.request_redraw(); } - - event::Status::Ignored } fn mouse_interaction( diff --git a/widget/src/row.rs b/widget/src/row.rs index 9c0fa97e..a4b6aa2a 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -1,13 +1,12 @@ //! Distribute content horizontally. use crate::core::alignment::{self, Alignment}; -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, + Clipboard, Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; @@ -264,24 +263,24 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( @@ -503,10 +502,10 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.row.on_event( tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index abad6ea6..33d4f545 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -21,7 +21,6 @@ //! ``` use crate::container; use crate::core::border::{self, Border}; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; @@ -34,8 +33,8 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - self, Background, Clipboard, Color, Element, Layout, Length, Padding, - Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, Background, Clipboard, Color, Element, Event, Layout, Length, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -519,7 +518,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); let cursor_over_scrollable = cursor.position_over(bounds); @@ -561,7 +560,7 @@ where if let Some(scrollbar) = scrollbars.y { let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; + return; }; state.scroll_y_to( @@ -581,7 +580,7 @@ where shell, ); - return event::Status::Captured; + shell.capture_event(); } } _ => {} @@ -593,7 +592,7 @@ where )) | Event::Touch(touch::Event::FingerPressed { .. }) => { let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; + return; }; if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( @@ -621,7 +620,7 @@ where ); } - return event::Status::Captured; + shell.capture_event(); } _ => {} } @@ -632,7 +631,7 @@ where Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; + return; }; if let Some(scrollbar) = scrollbars.x { @@ -654,7 +653,7 @@ where ); } - return event::Status::Captured; + shell.capture_event(); } _ => {} } @@ -665,7 +664,7 @@ where )) | Event::Touch(touch::Event::FingerPressed { .. }) => { let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; + return; }; if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( @@ -692,20 +691,19 @@ where shell, ); - return event::Status::Captured; + shell.capture_event(); } } _ => {} } } - let content_status = if state.last_scrolled.is_some() - && matches!( + if state.last_scrolled.is_none() + || !matches!( event, Event::Mouse(mouse::Event::WheelScrolled { .. }) - ) { - event::Status::Ignored - } else { + ) + { let cursor = match cursor_over_scrollable { Some(cursor_position) if !(mouse_over_x_scrollbar @@ -739,7 +737,7 @@ where x: bounds.x + translation.x, ..bounds }, - ) + ); }; if matches!( @@ -754,11 +752,11 @@ where state.x_scroller_grabbed_at = None; state.y_scroller_grabbed_at = None; - return content_status; + return; } - if let event::Status::Captured = content_status { - return event::Status::Captured; + if shell.is_event_captured() { + return; } if let Event::Keyboard(keyboard::Event::ModifiersChanged( @@ -767,13 +765,13 @@ where { state.keyboard_modifiers = modifiers; - return event::Status::Ignored; + return; } match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { if cursor_over_scrollable.is_none() { - return event::Status::Ignored; + return; } let delta = match delta { @@ -827,9 +825,7 @@ where let in_transaction = state.last_scrolled.is_some(); if has_scrolled || in_transaction { - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Touch(event) @@ -841,7 +837,7 @@ where touch::Event::FingerPressed { .. } => { let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; + return; }; state.scroll_area_touched_at = @@ -853,7 +849,7 @@ where { let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; + return; }; let delta = Vector::new( @@ -883,7 +879,7 @@ where _ => {} } - event::Status::Captured + shell.capture_event(); } Event::Window(window::Event::RedrawRequested(_)) => { let _ = notify_viewport( @@ -893,14 +889,12 @@ where content_bounds, shell, ); - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } }; - let event_status = update(); + update(); let status = if state.y_scroller_grabbed_at.is_some() || state.x_scroller_grabbed_at.is_some() @@ -933,8 +927,6 @@ where { shell.request_redraw(); } - - event_status } fn draw( diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fa692336..5e4d3915 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -97,7 +97,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); let custom_shader_event = match event { @@ -115,22 +115,14 @@ where if let Some(custom_shader_event) = custom_shader_event { let state = tree.state.downcast_mut::(); - let (event_status, message) = self.program.update( + self.program.update( state, custom_shader_event, bounds, cursor, shell, ); - - if let Some(message) = message { - shell.publish(message); - } - - return event_status; } - - event::Status::Ignored } fn mouse_interaction( @@ -195,8 +187,8 @@ where bounds: Rectangle, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - ) -> (event::Status, Option) { - T::update(self, state, event, bounds, cursor, shell) + ) { + T::update(self, state, event, bounds, cursor, shell); } fn draw( diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs index 005c8725..2d7c79bb 100644 --- a/widget/src/shader/event.rs +++ b/widget/src/shader/event.rs @@ -4,8 +4,6 @@ use crate::core::mouse; use crate::core::time::Instant; use crate::core::touch; -pub use crate::core::event::Status; - /// A [`Shader`] event. /// /// [`Shader`]: crate::Shader diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 902c7c3b..5124a1cc 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -1,4 +1,3 @@ -use crate::core::event; use crate::core::mouse; use crate::core::{Rectangle, Shell}; use crate::renderer::wgpu::Primitive; @@ -31,8 +30,7 @@ pub trait Program { _bounds: Rectangle, _cursor: mouse::Cursor, _shell: &mut Shell<'_, Message>, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) + ) { } /// Draws the [`Primitive`]. diff --git a/widget/src/slider.rs b/widget/src/slider.rs index dbdb5f07..4a424bd9 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -29,7 +29,6 @@ //! } //! ``` use crate::core::border::{self, Border}; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; use crate::core::layout; @@ -39,8 +38,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Theme, Widget, + self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels, + Point, Rectangle, Shell, Size, Theme, Widget, }; use std::ops::RangeInclusive; @@ -253,7 +252,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); let mut update = || { @@ -349,7 +348,7 @@ where state.is_dragging = true; } - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::ButtonReleased( @@ -363,7 +362,7 @@ where } state.is_dragging = false; - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::CursorMoved { .. }) @@ -371,7 +370,7 @@ where if state.is_dragging { let _ = cursor.position().and_then(locate).map(change); - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { delta }) @@ -389,7 +388,7 @@ where let _ = increment(current_value).map(change); } - return event::Status::Captured; + shell.capture_event(); } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -406,7 +405,7 @@ where _ => (), } - return event::Status::Captured; + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged( @@ -416,11 +415,9 @@ where } _ => {} } - - event::Status::Ignored }; - let update_status = update(); + update(); let current_status = if state.is_dragging { Status::Dragged @@ -435,8 +432,6 @@ where } else if self.status.is_some_and(|status| status != current_status) { shell.request_redraw(); } - - update_status } fn draw( diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6a44c328..2cb628ab 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,12 +1,12 @@ //! Display content on top of other content. -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, + Widget, }; /// A container that displays children on top of each other. @@ -214,40 +214,41 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let is_over = cursor.is_over(layout.bounds()); - self.children + for ((child, state), layout) in self + .children .iter_mut() .rev() .zip(tree.children.iter_mut().rev()) .zip(layout.children().rev()) - .map(|((child, state), layout)| { - let status = child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, + { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + if is_over && cursor != mouse::Cursor::Unavailable { + let interaction = child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, ); - if is_over && cursor != mouse::Cursor::Unavailable { - let interaction = child.as_widget().mouse_interaction( - state, layout, cursor, viewport, renderer, - ); - - if interaction != mouse::Interaction::None { - cursor = mouse::Cursor::Unavailable; - } + if interaction != mouse::Interaction::None { + cursor = mouse::Cursor::Unavailable; } + } - status - }) - .find(|&status| status == event::Status::Captured) - .unwrap_or(event::Status::Ignored) + if shell.is_event_captured() { + return; + } + } } fn mouse_interaction( diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 3d241375..f778b029 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -1,5 +1,4 @@ use crate::core::alignment; -use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -365,7 +364,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Link>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { if let Some(position) = cursor.position_in(layout.bounds()) { @@ -374,9 +373,16 @@ where .downcast_mut::>(); if let Some(span) = state.paragraph.hit_span(position) { - state.span_pressed = Some(span); - - return event::Status::Captured; + if self + .spans + .as_ref() + .as_ref() + .get(span) + .is_some_and(|span| span.link.is_some()) + { + state.span_pressed = Some(span); + shell.capture_event(); + } } } } @@ -409,8 +415,6 @@ where } _ => {} } - - event::Status::Ignored } fn mouse_interaction( diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 3bb45494..292e584e 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -33,7 +33,6 @@ //! ``` use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; @@ -47,7 +46,7 @@ use crate::core::widget::operation; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Length, Padding, Pixels, Point, + Background, Border, Color, Element, Event, Length, Padding, Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; @@ -606,9 +605,9 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let Some(on_edit) = self.on_edit.as_ref() else { - return event::Status::Ignored; + return; }; let state = tree.state.downcast_mut::>(); @@ -656,7 +655,7 @@ where cursor, self.key_binding.as_deref(), ) else { - return event::Status::Ignored; + return; }; match update { @@ -685,7 +684,7 @@ where let bounds = self.content.0.borrow().editor.bounds(); if bounds.height >= i32::MAX as f32 { - return event::Status::Ignored; + return; } let lines = lines + state.partial_scroll; @@ -798,7 +797,7 @@ where } } - event::Status::Captured + shell.capture_event(); } fn draw( diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 8fa7889f..87dffb98 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -42,7 +42,6 @@ use editor::Editor; use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout; @@ -57,8 +56,8 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Size, Theme, Vector, Widget, + Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels, + Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -638,7 +637,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let update_cache = |state, value| { replace_paragraph( renderer, @@ -754,7 +753,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -809,7 +808,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -834,14 +833,15 @@ where ); } - return event::Status::Captured; + shell.capture_event(); + return; } keyboard::Key::Character("x") if state.keyboard_modifiers.command() && !self.is_secure => { let Some(on_input) = &self.on_input else { - return event::Status::Ignored; + return; }; if let Some((start, end)) = @@ -859,18 +859,18 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; + return; } keyboard::Key::Character("v") if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() => { let Some(on_input) = &self.on_input else { - return event::Status::Ignored; + return; }; let content = match state.is_pasting.take() { @@ -897,12 +897,12 @@ where (on_input)(editor.contents()) }; shell.publish(message); + shell.capture_event(); state.is_pasting = Some(content); focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; + return; } keyboard::Key::Character("a") if state.keyboard_modifiers.command() => @@ -917,14 +917,15 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} } if let Some(text) = text { let Some(on_input) = &self.on_input else { - return event::Status::Ignored; + return; }; state.is_pasting = None; @@ -939,11 +940,11 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; + return; } } @@ -951,13 +952,12 @@ where keyboard::Key::Named(key::Named::Enter) => { if let Some(on_submit) = self.on_submit.clone() { shell.publish(on_submit); - - return event::Status::Captured; + shell.capture_event(); } } keyboard::Key::Named(key::Named::Backspace) => { let Some(on_input) = &self.on_input else { - return event::Status::Ignored; + return; }; if modifiers.jump() @@ -980,15 +980,14 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; } keyboard::Key::Named(key::Named::Delete) => { let Some(on_input) = &self.on_input else { - return event::Status::Ignored; + return; }; if modifiers.jump() @@ -1014,11 +1013,10 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; } keyboard::Key::Named(key::Named::Home) => { let cursor_before = state.cursor; @@ -1038,7 +1036,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } keyboard::Key::Named(key::Named::End) => { let cursor_before = state.cursor; @@ -1058,7 +1056,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } keyboard::Key::Named(key::Named::ArrowLeft) if modifiers.macos_command() => @@ -1080,7 +1078,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } keyboard::Key::Named(key::Named::ArrowRight) if modifiers.macos_command() => @@ -1102,7 +1100,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } keyboard::Key::Named(key::Named::ArrowLeft) => { let cursor_before = state.cursor; @@ -1129,7 +1127,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } keyboard::Key::Named(key::Named::ArrowRight) => { let cursor_before = state.cursor; @@ -1156,7 +1154,7 @@ where shell.request_redraw(); } - return event::Status::Captured; + shell.capture_event(); } keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; @@ -1166,7 +1164,7 @@ where state.keyboard_modifiers = keyboard::Modifiers::default(); - return event::Status::Captured; + shell.capture_event(); } _ => {} } @@ -1179,7 +1177,7 @@ where if let keyboard::Key::Character("v") = key.as_ref() { state.is_pasting = None; - return event::Status::Captured; + shell.capture_event(); } } @@ -1257,8 +1255,6 @@ where { shell.request_redraw(); } - - event::Status::Ignored } fn draw( diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 499a9fe8..649cfbdd 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,5 +1,4 @@ use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -7,8 +6,8 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Shell, Size, Vector, Widget, + Background, Clipboard, Color, Element, Event, Layout, Length, Point, + Rectangle, Shell, Size, Vector, Widget, }; use std::marker::PhantomData; @@ -121,10 +120,10 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.content.as_widget_mut().on_event( tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -227,9 +226,10 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content - .on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self.content.on_event( + event, layout, cursor, renderer, clipboard, shell, + ); } fn operate( diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 2553a7e4..8461fbb2 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -31,7 +31,6 @@ //! } //! ``` use crate::core::alignment; -use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -317,26 +316,23 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let Some(on_toggle) = &self.on_toggle else { - return event::Status::Ignored; + return; }; - let event_status = match event { + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { shell.publish(on_toggle(!self.is_toggled)); - - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } - _ => event::Status::Ignored, - }; + _ => {} + } let current_status = if self.on_toggle.is_none() { Status::Disabled @@ -358,8 +354,6 @@ where { shell.request_redraw(); } - - event_status } fn mouse_interaction( diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index e98f4da7..91151acc 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -22,7 +22,6 @@ //! } //! ``` use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -30,8 +29,8 @@ use crate::core::renderer; use crate::core::text; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, - Vector, + Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle, + Shell, Size, Vector, }; /// An element to display a widget over another. @@ -200,7 +199,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); let was_idle = *state == State::Idle; @@ -225,7 +224,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 18633474..e7e36d2a 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -35,7 +35,6 @@ pub use crate::slider::{ }; use crate::core::border::Border; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; use crate::core::layout::{self, Layout}; @@ -44,8 +43,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size, - Widget, + self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell, + Size, Widget, }; /// An vertical bar and a handle that selects a single value from a range of @@ -254,7 +253,7 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); let is_dragging = state.is_dragging; let current_value = self.value; @@ -350,7 +349,7 @@ where state.is_dragging = true; } - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -362,7 +361,7 @@ where } state.is_dragging = false; - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::CursorMoved { .. }) @@ -370,7 +369,7 @@ where if is_dragging { let _ = cursor.position().and_then(locate).map(change); - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { delta }) @@ -388,7 +387,7 @@ where let _ = increment(current_value).map(change); } - return event::Status::Captured; + shell.capture_event(); } } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { @@ -403,7 +402,7 @@ where _ => (), } - return event::Status::Captured; + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { @@ -411,8 +410,6 @@ where } _ => {} } - - event::Status::Ignored } fn draw( From f02bfc3f68322bea0c56283d76888714be401ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Oct 2024 22:06:06 +0200 Subject: [PATCH 403/657] Rename `Widget::on_event` to `update` --- core/src/element.rs | 8 ++++---- core/src/widget.rs | 2 +- examples/loading_spinners/src/circular.rs | 2 +- examples/loading_spinners/src/linear.rs | 2 +- examples/toast/src/main.rs | 6 +++--- runtime/src/user_interface.rs | 2 +- widget/src/button.rs | 4 ++-- widget/src/canvas.rs | 2 +- widget/src/checkbox.rs | 2 +- widget/src/column.rs | 4 ++-- widget/src/combo_box.rs | 6 +++--- widget/src/container.rs | 4 ++-- widget/src/helpers.rs | 10 +++++----- widget/src/image/viewer.rs | 2 +- widget/src/keyed/column.rs | 4 ++-- widget/src/lazy.rs | 4 ++-- widget/src/lazy/component.rs | 4 ++-- widget/src/lazy/responsive.rs | 4 ++-- widget/src/mouse_area.rs | 4 ++-- widget/src/overlay/menu.rs | 4 ++-- widget/src/pane_grid.rs | 2 +- widget/src/pane_grid/content.rs | 2 +- widget/src/pane_grid/title_bar.rs | 8 ++++---- widget/src/pick_list.rs | 2 +- widget/src/radio.rs | 2 +- widget/src/row.rs | 8 ++++---- widget/src/scrollable.rs | 4 ++-- widget/src/shader.rs | 2 +- widget/src/slider.rs | 2 +- widget/src/stack.rs | 4 ++-- widget/src/text/rich.rs | 2 +- widget/src/text_editor.rs | 2 +- widget/src/text_input.rs | 2 +- widget/src/themer.rs | 4 ++-- widget/src/toggler.rs | 2 +- widget/src/tooltip.rs | 4 ++-- widget/src/vertical_slider.rs | 2 +- 37 files changed, 67 insertions(+), 67 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index 8276b70c..03e56b43 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -308,7 +308,7 @@ where self.widget.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -322,7 +322,7 @@ where let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); - self.widget.on_event( + self.widget.update( tree, event, layout, @@ -444,7 +444,7 @@ where .operate(state, layout, renderer, operation); } - fn on_event( + fn update( &mut self, state: &mut Tree, event: Event, @@ -455,7 +455,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.element.widget.on_event( + self.element.widget.update( state, event, layout, cursor, renderer, clipboard, shell, viewport, ); } diff --git a/core/src/widget.rs b/core/src/widget.rs index bddf99cc..2a40f823 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -111,7 +111,7 @@ where /// Processes a runtime [`Event`]. /// /// By default, it does nothing. - fn on_event( + fn update( &mut self, _state: &mut Tree, _event: Event, diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index e6b59cae..a10d5cec 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -261,7 +261,7 @@ where layout::atomic(limits, self.size, self.size) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 34576261..91c8d523 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -175,7 +175,7 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 079b96b4..893c52e2 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -358,7 +358,7 @@ mod toast { }); } - fn on_event( + fn update( &mut self, state: &mut Tree, event: Event, @@ -369,7 +369,7 @@ mod toast { shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut state.children[0], event, layout, @@ -529,7 +529,7 @@ mod toast { let mut local_messages = vec![]; let mut local_shell = Shell::new(&mut local_messages); - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, event.clone(), layout, diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index cae17bcc..6997caf4 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -308,7 +308,7 @@ where let mut shell = Shell::new(messages); - self.root.as_widget_mut().on_event( + self.root.as_widget_mut().update( &mut self.state, event, Layout::new(&self.base), diff --git a/widget/src/button.rs b/widget/src/button.rs index 6be17f59..64f2b793 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -273,7 +273,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -284,7 +284,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event.clone(), layout.children().next().unwrap(), diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index a9c65bb6..63a25064 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -213,7 +213,7 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: core::Event, diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 9b5f3602..625dee7c 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -302,7 +302,7 @@ where ) } - fn on_event( + fn update( &mut self, _tree: &mut Tree, event: Event, diff --git a/widget/src/column.rs b/widget/src/column.rs index 3fdc17f8..a3efab94 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -257,7 +257,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -274,7 +274,7 @@ where .zip(&mut tree.children) .zip(layout.children()) { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, event.clone(), layout, diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 1122861f..6c5d2309 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -510,7 +510,7 @@ where vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)] } - fn on_event( + fn update( &mut self, tree: &mut widget::Tree, event: Event, @@ -539,7 +539,7 @@ where let mut local_shell = Shell::new(&mut local_messages); // Provide it to the widget - self.text_input.on_event( + self.text_input.update( &mut tree.children[0], event.clone(), layout, @@ -728,7 +728,7 @@ where published_message_to_shell = true; // Unfocus the input - self.text_input.on_event( + self.text_input.update( &mut tree.children[0], Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, diff --git a/widget/src/container.rs b/widget/src/container.rs index f96c495c..b7b2b39e 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -297,7 +297,7 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -308,7 +308,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( tree, event, layout.children().next().unwrap(), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 13d69e1f..e1474d34 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -438,7 +438,7 @@ where .operate(state, layout, renderer, operation); } - fn on_event( + fn update( &mut self, state: &mut Tree, event: Event, @@ -454,7 +454,7 @@ where core::Event::Mouse(mouse::Event::ButtonPressed(_)) ); - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( state, event, layout, cursor, renderer, clipboard, shell, viewport, ); @@ -640,7 +640,7 @@ where } } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -682,7 +682,7 @@ where || self.is_top_focused || self.is_top_overlay_active { - self.top.as_widget_mut().on_event( + self.top.as_widget_mut().update( top_tree, event.clone(), top_layout, @@ -698,7 +698,7 @@ where return; } - self.base.as_widget_mut().on_event( + self.base.as_widget_mut().update( base_tree, event.clone(), base_layout, diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 5787200b..20a7955f 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -148,7 +148,7 @@ where layout::Node::new(final_size) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 1172785a..055e2ea1 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -297,7 +297,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -314,7 +314,7 @@ where .zip(&mut tree.children) .zip(layout.children()) { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, event.clone(), layout, diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 4a9a6154..0b4e3cad 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -195,7 +195,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -207,7 +207,7 @@ where viewport: &Rectangle, ) { self.with_element_mut(|element| { - element.as_widget_mut().on_event( + element.as_widget_mut().update( &mut tree.children[0], event, layout, diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 062e6f35..6f661ef6 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -311,7 +311,7 @@ where }) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: core::Event, @@ -327,7 +327,7 @@ where let t = tree.state.downcast_mut::>>>(); self.with_element_mut(|element| { - element.as_widget_mut().on_event( + element.as_widget_mut().update( &mut t.borrow_mut().as_mut().unwrap().children[0], event, layout, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index c17798a6..a8abbce8 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -182,7 +182,7 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -205,7 +205,7 @@ where layout, &self.view, |tree, renderer, layout, element| { - element.as_widget_mut().on_event( + element.as_widget_mut().update( tree, event, layout, diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 50188abd..d9215a7b 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -215,7 +215,7 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -226,7 +226,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event.clone(), layout, diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 78ee3da6..b0c0bbad 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -273,7 +273,7 @@ where ) { let bounds = layout.bounds(); - self.list.on_event( + self.list.update( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ); @@ -386,7 +386,7 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 29b7ac87..a966627a 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -422,7 +422,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 81a5cc1e..e0199f0a 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -271,7 +271,7 @@ where }; if !is_picked { - self.body.as_widget_mut().on_event( + self.body.as_widget_mut().update( &mut tree.children[0], event, body_layout, diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index ec1dc302..618eb4c5 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -454,7 +454,7 @@ where if let Some(compact) = controls.compact.as_mut() { let compact_layout = children.next().unwrap(); - compact.as_widget_mut().on_event( + compact.as_widget_mut().update( &mut tree.children[2], event.clone(), compact_layout, @@ -467,7 +467,7 @@ where } else { show_title = false; - controls.full.as_widget_mut().on_event( + controls.full.as_widget_mut().update( &mut tree.children[1], event.clone(), controls_layout, @@ -479,7 +479,7 @@ where ); } } else { - controls.full.as_widget_mut().on_event( + controls.full.as_widget_mut().update( &mut tree.children[1], event.clone(), controls_layout, @@ -493,7 +493,7 @@ where } if show_title { - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, title_layout, diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 9eb43e72..32f859da 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -427,7 +427,7 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 70e1c423..b38ae6b4 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -323,7 +323,7 @@ where ) } - fn on_event( + fn update( &mut self, _state: &mut Tree, event: Event, diff --git a/widget/src/row.rs b/widget/src/row.rs index a4b6aa2a..96a4ab92 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -253,7 +253,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -270,7 +270,7 @@ where .zip(&mut tree.children) .zip(layout.children()) { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, event.clone(), layout, @@ -492,7 +492,7 @@ where self.row.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -503,7 +503,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.row.on_event( + self.row.update( tree, event, layout, cursor, renderer, clipboard, shell, viewport, ); } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 33d4f545..a6a41d9f 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -508,7 +508,7 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -724,7 +724,7 @@ where let translation = state.translation(self.direction, bounds, content_bounds); - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event.clone(), content, diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 5e4d3915..115a5ed9 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -87,7 +87,7 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: crate::core::Event, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 4a424bd9..84630f9e 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -242,7 +242,7 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 2cb628ab..52fd5031 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -204,7 +204,7 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -224,7 +224,7 @@ where .zip(tree.children.iter_mut().rev()) .zip(layout.children().rev()) { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, event.clone(), layout, diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index f778b029..7ef2707b 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -354,7 +354,7 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 292e584e..32e14946 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -595,7 +595,7 @@ where } } - fn on_event( + fn update( &mut self, tree: &mut widget::Tree, event: Event, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 87dffb98..51e61cc0 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -627,7 +627,7 @@ where operation.text_input(state, self.id.as_ref().map(|id| &id.0)); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 649cfbdd..41d2aeae 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -110,7 +110,7 @@ where .operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, @@ -121,7 +121,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( tree, event, layout, cursor, renderer, clipboard, shell, viewport, ); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 8461fbb2..5dfa0c0e 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -306,7 +306,7 @@ where ) } - fn on_event( + fn update( &mut self, _state: &mut Tree, event: Event, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 91151acc..e66f5e4a 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -189,7 +189,7 @@ where .layout(&mut tree.children[0], renderer, limits) } - fn on_event( + fn update( &mut self, tree: &mut widget::Tree, event: Event, @@ -215,7 +215,7 @@ where shell.invalidate_layout(); } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index e7e36d2a..ec72a455 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -243,7 +243,7 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, event: Event, From a84b328dcc3e2f941f9595a2f8c3b1d061442722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Oct 2024 22:36:55 +0200 Subject: [PATCH 404/657] Implement `reactive-rendering` for `combo_box` --- widget/src/combo_box.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 6c5d2309..8b8e895d 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -54,7 +54,6 @@ //! } //! } //! ``` -use crate::core::event; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; @@ -64,6 +63,7 @@ use crate::core::renderer; use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; +use crate::core::window; use crate::core::{ Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme, Vector, @@ -550,10 +550,21 @@ where viewport, ); - if local_shell.event_status() == event::Status::Captured { + if local_shell.is_event_captured() { shell.capture_event(); } + if let Some(redraw_request) = local_shell.redraw_request() { + match redraw_request { + window::RedrawRequest::NextFrame => { + shell.request_redraw(); + } + window::RedrawRequest::At(at) => { + shell.request_redraw_at(at); + } + } + } + // Then finally react to them here for message in local_messages { let TextInputEvent::TextChanged(new_value) = message; @@ -580,6 +591,7 @@ where ); }); shell.invalidate_layout(); + shell.request_redraw(); } let is_focused = { @@ -624,8 +636,8 @@ where } shell.capture_event(); + shell.request_redraw(); } - (key::Named::ArrowUp, _) | (key::Named::Tab, true) => { if let Some(index) = &mut menu.hovered_option { if *index == 0 { @@ -661,6 +673,7 @@ where } shell.capture_event(); + shell.request_redraw(); } (key::Named::ArrowDown, _) | (key::Named::Tab, false) @@ -708,6 +721,7 @@ where } shell.capture_event(); + shell.request_redraw(); } _ => {} } From 920596ed6f44acf8d87d2135c1b8967bab23d5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 28 Oct 2024 16:58:00 +0100 Subject: [PATCH 405/657] Implement `reactive-rendering` for `canvas` --- examples/bezier_tool/src/main.rs | 76 ++++++++++---------- examples/game_of_life/src/main.rs | 52 +++++++++----- examples/multitouch/src/main.rs | 33 +++++---- examples/sierpinski_triangle/src/main.rs | 33 ++++----- widget/src/action.rs | 89 ++++++++++++++++++++++++ widget/src/canvas.rs | 63 +++++++++++------ widget/src/canvas/event.rs | 21 ------ widget/src/canvas/program.rs | 15 ++-- widget/src/lib.rs | 2 + 9 files changed, 243 insertions(+), 141 deletions(-) create mode 100644 widget/src/action.rs delete mode 100644 widget/src/canvas/event.rs diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 949bfad7..5e4da0c2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -57,8 +57,9 @@ impl Example { mod bezier { use iced::mouse; - use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; + use iced::widget::canvas::{ + self, Canvas, Event, Frame, Geometry, Path, Stroke, + }; use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; #[derive(Default)] @@ -96,48 +97,47 @@ mod bezier { event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + ) -> Option> { + let cursor_position = cursor.position_in(bounds)?; match event { - Event::Mouse(mouse_event) => { - let message = match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - match *state { - None => { - *state = Some(Pending::One { - from: cursor_position, - }); + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => Some( + match *state { + None => { + *state = Some(Pending::One { + from: cursor_position, + }); - None - } - Some(Pending::One { from }) => { - *state = Some(Pending::Two { - from, - to: cursor_position, - }); - - None - } - Some(Pending::Two { from, to }) => { - *state = None; - - Some(Curve { - from, - to, - control: cursor_position, - }) - } - } + canvas::Action::request_redraw() } - _ => None, - }; + Some(Pending::One { from }) => { + *state = Some(Pending::Two { + from, + to: cursor_position, + }); - (event::Status::Captured, message) + canvas::Action::request_redraw() + } + Some(Pending::Two { from, to }) => { + *state = None; + + canvas::Action::publish(Curve { + from, + to, + control: cursor_position, + }) + } + } + .and_capture(), + ), + Event::Mouse(mouse::Event::CursorMoved { .. }) + if state.is_some() => + { + Some(canvas::Action::request_redraw()) } - _ => (event::Status::Ignored, None), + _ => None, } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9dcebecc..7a7224d5 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -193,8 +193,9 @@ mod grid { use iced::mouse; use iced::touch; use iced::widget::canvas; - use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; + use iced::widget::canvas::{ + Cache, Canvas, Event, Frame, Geometry, Path, Text, + }; use iced::{ Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; @@ -383,14 +384,12 @@ mod grid { event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { + ) -> Option> { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { *interaction = Interaction::None; } - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + let cursor_position = cursor.position_in(bounds)?; let cell = Cell::at(self.project(cursor_position, bounds.size())); let is_populated = self.state.contains(&cell); @@ -413,7 +412,12 @@ mod grid { populate.or(unpopulate) }; - (event::Status::Captured, message) + Some( + message + .map(canvas::Action::publish) + .unwrap_or(canvas::Action::request_redraw()) + .and_capture(), + ) } Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(button) => { @@ -438,7 +442,12 @@ mod grid { _ => None, }; - (event::Status::Captured, message) + Some( + message + .map(canvas::Action::publish) + .unwrap_or(canvas::Action::request_redraw()) + .and_capture(), + ) } mouse::Event::CursorMoved { .. } => { let message = match *interaction { @@ -454,12 +463,14 @@ mod grid { Interaction::None => None, }; - let event_status = match interaction { - Interaction::None => event::Status::Ignored, - _ => event::Status::Captured, - }; + let action = message + .map(canvas::Action::publish) + .unwrap_or(canvas::Action::request_redraw()); - (event_status, message) + Some(match interaction { + Interaction::None => action, + _ => action.and_capture(), + }) } mouse::Event::WheelScrolled { delta } => match delta { mouse::ScrollDelta::Lines { y, .. } @@ -496,18 +507,21 @@ mod grid { None }; - ( - event::Status::Captured, - Some(Message::Scaled(scaling, translation)), + Some( + canvas::Action::publish(Message::Scaled( + scaling, + translation, + )) + .and_capture(), ) } else { - (event::Status::Captured, None) + Some(canvas::Action::capture()) } } }, - _ => (event::Status::Ignored, None), + _ => None, }, - _ => (event::Status::Ignored, None), + _ => None, } } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index d5e5dffa..5f4a5c90 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -3,9 +3,8 @@ //! computers like Microsoft Surface. use iced::mouse; use iced::touch; -use iced::widget::canvas::event; use iced::widget::canvas::stroke::{self, Stroke}; -use iced::widget::canvas::{self, Canvas, Geometry}; +use iced::widget::canvas::{self, Canvas, Event, Geometry}; use iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme}; use std::collections::HashMap; @@ -56,25 +55,25 @@ impl canvas::Program for Multitouch { fn update( &self, _state: &mut Self::State, - event: event::Event, + event: Event, _bounds: Rectangle, _cursor: mouse::Cursor, - ) -> (event::Status, Option) { - match event { - event::Event::Touch(touch_event) => match touch_event { + ) -> Option> { + let message = match event { + Event::Touch( touch::Event::FingerPressed { id, position } - | touch::Event::FingerMoved { id, position } => ( - event::Status::Captured, - Some(Message::FingerPressed { id, position }), - ), + | touch::Event::FingerMoved { id, position }, + ) => Some(Message::FingerPressed { id, position }), + Event::Touch( touch::Event::FingerLifted { id, .. } - | touch::Event::FingerLost { id, .. } => ( - event::Status::Captured, - Some(Message::FingerLifted { id }), - ), - }, - _ => (event::Status::Ignored, None), - } + | touch::Event::FingerLost { id, .. }, + ) => Some(Message::FingerLifted { id }), + _ => None, + }; + + message + .map(canvas::Action::publish) + .map(canvas::Action::and_capture) } fn draw( diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 99e7900a..d4d483f5 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -1,6 +1,5 @@ use iced::mouse; -use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{self, Canvas, Geometry}; +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}; @@ -80,26 +79,22 @@ impl canvas::Program for SierpinskiGraph { event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + ) -> Option> { + let cursor_position = cursor.position_in(bounds)?; match event { - Event::Mouse(mouse_event) => { - let message = match mouse_event { - iced::mouse::Event::ButtonPressed( - iced::mouse::Button::Left, - ) => Some(Message::PointAdded(cursor_position)), - iced::mouse::Event::ButtonPressed( - iced::mouse::Button::Right, - ) => Some(Message::PointRemoved), - _ => None, - }; - (event::Status::Captured, message) - } - _ => (event::Status::Ignored, None), + Event::Mouse(mouse::Event::ButtonPressed(button)) => match button { + mouse::Button::Left => Some(canvas::Action::publish( + Message::PointAdded(cursor_position), + )), + mouse::Button::Right => { + Some(canvas::Action::publish(Message::PointRemoved)) + } + _ => None, + }, + _ => None, } + .map(canvas::Action::and_capture) } fn draw( diff --git a/widget/src/action.rs b/widget/src/action.rs new file mode 100644 index 00000000..1dd3a787 --- /dev/null +++ b/widget/src/action.rs @@ -0,0 +1,89 @@ +use crate::core::event; +use crate::core::time::Instant; +use crate::core::window; + +/// A runtime action that can be performed by some widgets. +#[derive(Debug, Clone)] +pub struct Action { + message_to_publish: Option, + redraw_request: Option, + event_status: event::Status, +} + +impl Action { + fn new() -> Self { + Self { + message_to_publish: None, + redraw_request: None, + event_status: event::Status::Ignored, + } + } + + /// Creates a new "capturing" [`Action`]. A capturing [`Action`] + /// will make other widgets consider it final and prevent further + /// processing. + /// + /// Prevents "event bubbling". + pub fn capture() -> Self { + Self { + event_status: event::Status::Captured, + ..Self::new() + } + } + + /// Creates a new [`Action`] that publishes the given `Message` for + /// the application to handle. + /// + /// Publishing a `Message` always produces a redraw. + pub fn publish(message: Message) -> Self { + Self { + message_to_publish: Some(message), + ..Self::new() + } + } + + /// Creates a new [`Action`] that requests a redraw to happen as + /// soon as possible; without publishing any `Message`. + pub fn request_redraw() -> Self { + Self { + redraw_request: Some(window::RedrawRequest::NextFrame), + ..Self::new() + } + } + + /// Creates a new [`Action`] that requests a redraw to happen at + /// the given [`Instant`]; without publishing any `Message`. + /// + /// This can be useful to efficiently animate content, like a + /// blinking caret on a text input. + pub fn request_redraw_at(at: Instant) -> Self { + Self { + redraw_request: Some(window::RedrawRequest::At(at)), + ..Self::new() + } + } + + /// Marks the [`Action`] as "capturing". See [`Self::capture`]. + pub fn and_capture(mut self) -> Self { + self.event_status = event::Status::Captured; + self + } + + /// Converts the [`Action`] into its internal parts. + /// + /// This method is meant to be used by runtimes, libraries, or internal + /// widget implementations. + pub fn into_inner( + self, + ) -> ( + Option, + Option, + event::Status, + ) { + ( + self.message_to_publish, + self.redraw_request, + self.event_status, + ) + } +} diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 63a25064..23cc3f2b 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -48,24 +48,24 @@ //! canvas(Circle { radius: 50.0 }).into() //! } //! ``` -pub mod event; - mod program; -pub use event::Event; pub use program::Program; +pub use crate::core::event::Event; pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, }; +pub use crate::Action; -use crate::core; +use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; @@ -148,6 +148,7 @@ where message_: PhantomData, theme_: PhantomData, renderer_: PhantomData, + last_mouse_interaction: Option, } impl Canvas @@ -166,6 +167,7 @@ where message_: PhantomData, theme_: PhantomData, renderer_: PhantomData, + last_mouse_interaction: None, } } @@ -216,39 +218,60 @@ where fn update( &mut self, tree: &mut Tree, - event: core::Event, + event: Event, layout: Layout<'_>, cursor: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let bounds = layout.bounds(); - let canvas_event = match event { - core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - core::Event::Window(_) => None, - }; + let state = tree.state.downcast_mut::(); + let is_redraw_request = matches!( + event, + Event::Window(window::Event::RedrawRequested(_now)), + ); - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::(); - - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); + if let Some(action) = self.program.update(state, event, bounds, cursor) + { + let (message, redraw_request, event_status) = action.into_inner(); if let Some(message) = message { shell.publish(message); } + if let Some(redraw_request) = redraw_request { + match redraw_request { + window::RedrawRequest::NextFrame => { + shell.request_redraw(); + } + window::RedrawRequest::At(at) => { + shell.request_redraw_at(at); + } + } + } + if event_status == event::Status::Captured { shell.capture_event(); } } + + if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { + let mouse_interaction = self + .mouse_interaction(tree, layout, cursor, viewport, renderer); + + if is_redraw_request { + self.last_mouse_interaction = Some(mouse_interaction); + } else if self.last_mouse_interaction.is_some_and( + |last_mouse_interaction| { + last_mouse_interaction != mouse_interaction + }, + ) { + shell.request_redraw(); + } + } } fn mouse_interaction( diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs deleted file mode 100644 index a8eb47f7..00000000 --- a/widget/src/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::touch; - -pub use crate::core::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::Canvas -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index a7ded0f4..c68b2830 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,8 +1,8 @@ -use crate::canvas::event::{self, Event}; use crate::canvas::mouse; -use crate::canvas::Geometry; +use crate::canvas::{Event, Geometry}; use crate::core::Rectangle; use crate::graphics::geometry; +use crate::Action; /// The state and logic of a [`Canvas`]. /// @@ -22,8 +22,9 @@ where /// When a [`Program`] is used in a [`Canvas`], the runtime will call this /// method for each [`Event`]. /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. + /// This method can optionally return an [`Action`] to either notify an + /// application of any meaningful interactions, capture the event, or + /// request a redraw. /// /// By default, this method does and returns nothing. /// @@ -34,8 +35,8 @@ where _event: Event, _bounds: Rectangle, _cursor: mouse::Cursor, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) + ) -> Option> { + None } /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. @@ -84,7 +85,7 @@ where event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { + ) -> Option> { T::update(self, state, event, bounds, cursor) } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index a68720d6..776a04a0 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -8,6 +8,7 @@ pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; +mod action; mod column; mod mouse_area; mod row; @@ -131,4 +132,5 @@ pub use qr_code::QRCode; pub mod markdown; pub use crate::core::theme::{self, Theme}; +pub use action::Action; pub use renderer::Renderer; From 4e47450c336a235fe26090665aca1cc7b4d23384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 29 Oct 2024 15:55:47 +0100 Subject: [PATCH 406/657] Implement `reactive-rendering` for `pane_grid` --- widget/src/pane_grid.rs | 115 ++++++++++++++++++++++---------- widget/src/pane_grid/content.rs | 25 +++++++ 2 files changed, 105 insertions(+), 35 deletions(-) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index a966627a..691b0a93 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -86,6 +86,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ self, Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, @@ -166,6 +167,7 @@ pub struct PaneGrid< on_drag: Option Message + 'a>>, on_resize: Option<(f32, Box Message + 'a>)>, class: ::Class<'a>, + last_mouse_interaction: Option, } impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer> @@ -202,6 +204,7 @@ where on_drag: None, on_resize: None, class: ::default(), + last_mouse_interaction: None, } } @@ -292,6 +295,52 @@ where .then(|| self.on_drag.is_some()) .unwrap_or_default() } + + fn grid_interaction( + &self, + action: &state::Action, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) -> Option { + if action.picked_pane().is_some() { + return Some(mouse::Interaction::Grabbing); + } + + let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); + let node = self.internal.layout(); + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let splits = + node.split_regions(self.spacing, bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, + ) + .map(|(_, axis, _)| axis) + }) + }); + + if let Some(resize_axis) = resize_axis { + return Some(match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }); + } + + None + } } #[derive(Default)] @@ -600,6 +649,8 @@ where shell.capture_event(); } } + } else if action.picked_pane().is_some() { + shell.request_redraw(); } } } @@ -635,6 +686,31 @@ where is_picked, ); } + + if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { + let interaction = self + .grid_interaction(action, layout, cursor) + .or_else(|| { + self.contents.iter().zip(layout.children()).find_map( + |(content, layout)| { + content.grid_interaction( + layout, + cursor, + on_drag.is_some(), + ) + }, + ) + }) + .unwrap_or(mouse::Interaction::None); + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_mouse_interaction = Some(interaction); + } else if self.last_mouse_interaction.is_some_and( + |last_mouse_interaction| last_mouse_interaction != interaction, + ) { + shell.request_redraw(); + } + } } fn mouse_interaction( @@ -647,41 +723,10 @@ where ) -> mouse::Interaction { let Memory { action, .. } = tree.state.downcast_ref(); - if action.picked_pane().is_some() { - return mouse::Interaction::Grabbing; - } - - let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); - let node = self.internal.layout(); - - let resize_axis = - action.picked_split().map(|(_, axis)| axis).or_else(|| { - resize_leeway.and_then(|leeway| { - let cursor_position = cursor.position()?; - let bounds = layout.bounds(); - - let splits = - node.split_regions(self.spacing, bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split( - splits.iter(), - self.spacing + leeway, - relative_cursor, - ) - .map(|(_, axis, _)| axis) - }) - }); - - if let Some(resize_axis) = resize_axis { - return match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }; + if let Some(grid_interaction) = + self.grid_interaction(action, layout, cursor) + { + return grid_interaction; } self.panes diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index e0199f0a..f1f67023 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -284,6 +284,31 @@ where } } + pub(crate) fn grid_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + drag_enabled: bool, + ) -> Option { + let title_bar = self.title_bar.as_ref()?; + + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + let is_over_pick_area = cursor + .position() + .map(|cursor_position| { + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + }) + .unwrap_or_default(); + + if is_over_pick_area && drag_enabled { + return Some(mouse::Interaction::Grab); + } + + None + } + pub(crate) fn mouse_interaction( &self, tree: &Tree, From c6af79a1d06013343f9caf2de80597d627254084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 29 Oct 2024 20:53:29 +0100 Subject: [PATCH 407/657] Fix deferred layout on resize after drawing --- widget/src/button.rs | 7 ++- widget/src/lazy/responsive.rs | 23 +++++---- winit/src/program.rs | 92 +++++++++++++---------------------- 3 files changed, 49 insertions(+), 73 deletions(-) diff --git a/widget/src/button.rs b/widget/src/button.rs index 64f2b793..9eac2e4c 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -17,7 +17,6 @@ //! } //! ``` use crate::core::border::{self, Border}; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -28,8 +27,8 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::window; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, - Shadow, Shell, Size, Theme, Vector, Widget, + Background, Clipboard, Color, Element, Event, Layout, Length, Padding, + Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; /// A generic widget that produces a message when pressed. @@ -295,7 +294,7 @@ where viewport, ); - if shell.event_status() == event::Status::Captured { + if shell.is_event_captured() { return; } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index a8abbce8..f9bd0334 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -82,18 +82,21 @@ where new_size: Size, view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>, ) { - let is_tree_empty = - tree.tag == tree::Tag::stateless() && tree.children.is_empty(); + if self.size != new_size { + self.element = view(new_size); + self.size = new_size; + self.layout = None; - if !is_tree_empty && self.size == new_size { - return; + tree.diff(&self.element); + } else { + let is_tree_empty = + tree.tag == tree::Tag::stateless() && tree.children.is_empty(); + + if is_tree_empty { + self.layout = None; + tree.diff(&self.element); + } } - - self.element = view(new_size); - self.size = new_size; - self.layout = None; - - tree.diff(&self.element); } fn resolve( diff --git a/winit/src/program.rs b/winit/src/program.rs index fb30ccd9..d7afb969 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -818,6 +818,39 @@ async fn run_instance( continue; }; + let physical_size = window.state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 + { + continue; + } + + if window.viewport_version + != window.state.viewport_version() + { + let logical_size = window.state.logical_size(); + + debug.layout_started(); + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + debug.layout_finished(); + + compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + let redraw_event = core::Event::Window( window::Event::RedrawRequested(Instant::now()), ); @@ -877,65 +910,6 @@ async fn run_instance( } } - let physical_size = window.state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 - { - continue; - } - - if window.viewport_version - != window.state.viewport_version() - { - let logical_size = window.state.logical_size(); - - debug.layout_started(); - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); - debug.layout_finished(); - - debug.draw_started(); - let new_mouse_interaction = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - window.state.cursor(), - ); - debug.draw_finished(); - - if new_mouse_interaction != window.mouse_interaction - { - window.raw.set_cursor( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = - new_mouse_interaction; - } - - compositor.configure_surface( - &mut window.surface, - physical_size.width, - physical_size.height, - ); - - window.viewport_version = - window.state.viewport_version(); - } - debug.render_started(); match compositor.present( &mut window.renderer, From 14ec3307304fbf40e7f281d2356f40456124dfef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Nov 2024 18:08:12 +0100 Subject: [PATCH 408/657] Replace `reactive-rendering` feature with `unconditional-rendering` --- Cargo.toml | 6 +++--- winit/Cargo.toml | 2 +- winit/src/program.rs | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bf85ada2..fc38a89d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme", "reactive-rendering"] +default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"] # Enables the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `tiny-skia` software renderer backend @@ -65,8 +65,8 @@ fira-sans = ["iced_renderer/fira-sans"] auto-detect-theme = ["iced_core/auto-detect-theme"] # Enables strict assertions for debugging purposes at the expense of performance strict-assertions = ["iced_renderer/strict-assertions"] -# Redraws only when widgets react to some runtime event -reactive-rendering = ["iced_winit/reactive-rendering"] +# Redraws on every runtime event, and not only when a widget requests it +unconditional-rendering = ["iced_winit/unconditional-rendering"] [dependencies] iced_core.workspace = true diff --git a/winit/Cargo.toml b/winit/Cargo.toml index b8f5a723..10a6369b 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -22,7 +22,7 @@ x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] -reactive-rendering = [] +unconditional-rendering = [] [dependencies] iced_futures.workspace = true diff --git a/winit/src/program.rs b/winit/src/program.rs index d7afb969..130bf220 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1051,11 +1051,13 @@ async fn run_instance( &mut messages, ); - #[cfg(not(feature = "reactive-rendering"))] + #[cfg(feature = "unconditional-rendering")] window.raw.request_redraw(); match ui_state { - #[cfg(feature = "reactive-rendering")] + #[cfg(not( + feature = "unconditional-rendering" + ))] user_interface::State::Updated { redraw_request: Some(redraw_request), } => match redraw_request { From 6fc16769c4fb07d5e976a9c1762ecd17cf734bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Nov 2024 18:26:46 +0100 Subject: [PATCH 409/657] Unify `shader::Program` API with `canvas::Program` --- widget/src/shader.rs | 54 ++++++++++++++++++------------------ widget/src/shader/event.rs | 23 --------------- widget/src/shader/program.rs | 14 +++++----- 3 files changed, 34 insertions(+), 57 deletions(-) delete mode 100644 widget/src/shader/event.rs diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 115a5ed9..8ec57482 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -1,23 +1,22 @@ //! A custom shader widget for wgpu applications. -mod event; mod program; -pub use event::Event; pub use program::Program; -use crate::core; +use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::window; -use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; +use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size}; use crate::renderer::wgpu::primitive; use std::marker::PhantomData; pub use crate::graphics::Viewport; +pub use crate::Action; pub use primitive::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. @@ -100,28 +99,30 @@ where ) { let bounds = layout.bounds(); - let custom_shader_event = match event { - core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - core::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Window(window::Event::RedrawRequested(instant)) => { - Some(Event::RedrawRequested(instant)) - } - core::Event::Window(_) => None, - }; + let state = tree.state.downcast_mut::(); - if let Some(custom_shader_event) = custom_shader_event { - let state = tree.state.downcast_mut::(); + if let Some(action) = self.program.update(state, event, bounds, cursor) + { + let (message, redraw_request, event_status) = action.into_inner(); - self.program.update( - state, - custom_shader_event, - bounds, - cursor, - shell, - ); + if let Some(message) = message { + shell.publish(message); + } + + if let Some(redraw_request) = redraw_request { + match redraw_request { + window::RedrawRequest::NextFrame => { + shell.request_redraw(); + } + window::RedrawRequest::At(at) => { + shell.request_redraw_at(at); + } + } + } + + if event_status == event::Status::Captured { + shell.capture_event(); + } } } @@ -186,9 +187,8 @@ where event: Event, bounds: Rectangle, cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - ) { - T::update(self, state, event, bounds, cursor, shell); + ) -> Option> { + T::update(self, state, event, bounds, cursor) } fn draw( diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs deleted file mode 100644 index 2d7c79bb..00000000 --- a/widget/src/shader/event.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Handle events of a custom shader widget. -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::time::Instant; -use crate::core::touch; - -/// A [`Shader`] event. -/// -/// [`Shader`]: crate::Shader -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), - - /// A window requested a redraw. - RedrawRequested(Instant), -} diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 5124a1cc..0fc110af 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -1,7 +1,7 @@ use crate::core::mouse; -use crate::core::{Rectangle, Shell}; +use crate::core::Rectangle; use crate::renderer::wgpu::Primitive; -use crate::shader; +use crate::shader::{self, Action}; /// The state and logic of a [`Shader`] widget. /// @@ -17,10 +17,10 @@ pub trait Program { type Primitive: Primitive + 'static; /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes - /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a - /// redraw for the window, etc. + /// based on mouse & other events. You can return an [`Action`] to publish a message, request a + /// redraw, or capture the event. /// - /// By default, this method does and returns nothing. + /// By default, this method returns `None`. /// /// [`State`]: Self::State fn update( @@ -29,8 +29,8 @@ pub trait Program { _event: shader::Event, _bounds: Rectangle, _cursor: mouse::Cursor, - _shell: &mut Shell<'_, Message>, - ) { + ) -> Option> { + None } /// Draws the [`Primitive`]. From d5a886dbcb50964ce8c6132a4bb3504d6396d01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Nov 2024 23:05:44 +0100 Subject: [PATCH 410/657] Fix `hover` widget not redrawing when hovered --- widget/src/helpers.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e1474d34..33dff647 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -534,6 +534,7 @@ where top: Element<'a, Message, Theme, Renderer>, is_top_focused: bool, is_top_overlay_active: bool, + is_hovered: bool, } impl<'a, Message, Theme, Renderer> Widget @@ -655,6 +656,8 @@ where let (base_layout, base_tree) = children.next().unwrap(); let (top_layout, top_tree) = children.next().unwrap(); + let is_hovered = cursor.is_over(layout.bounds()); + if matches!(event, Event::Window(window::Event::RedrawRequested(_))) { let mut count_focused = operation::focusable::count(); @@ -670,6 +673,10 @@ where operation::Outcome::Some(count) => count.focused.is_some(), _ => false, }; + + self.is_hovered = is_hovered; + } else if is_hovered != self.is_hovered { + shell.request_redraw(); } if matches!( @@ -678,7 +685,7 @@ where mouse::Event::CursorMoved { .. } | mouse::Event::ButtonReleased(_) ) - ) || cursor.is_over(layout.bounds()) + ) || is_hovered || self.is_top_focused || self.is_top_overlay_active { @@ -767,6 +774,7 @@ where top: top.into(), is_top_focused: false, is_top_overlay_active: false, + is_hovered: false, }) } From 3482ffecdcadf036b7d61ab3821c6ee661d0ec56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Nov 2024 23:21:06 +0100 Subject: [PATCH 411/657] Implement `reactive-rendering` for `text_editor` --- widget/src/text_editor.rs | 309 ++++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 143 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 32e14946..3bd4c7f9 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -119,6 +119,7 @@ pub struct TextEditor< &Highlighter::Highlight, &Theme, ) -> highlighter::Format, + last_status: Option, } impl<'a, Message, Theme, Renderer> @@ -146,6 +147,7 @@ where highlighter_format: |_highlight, _theme| { highlighter::Format::default() }, + last_status: None, } } } @@ -269,6 +271,7 @@ where on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, + last_status: self.last_status, } } @@ -611,6 +614,10 @@ where }; let state = tree.state.downcast_mut::>(); + let is_redraw = matches!( + event, + Event::Window(window::Event::RedrawRequested(_now)), + ); match event { Event::Window(window::Event::Unfocused) => { @@ -647,157 +654,180 @@ where _ => {} } - let Some(update) = Update::from_event( + if let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, self.key_binding.as_deref(), - ) else { - return; - }; + ) { + shell.capture_event(); - match update { - Update::Click(click) => { - let action = match click.kind() { - mouse::click::Kind::Single => { - Action::Click(click.position()) - } - mouse::click::Kind::Double => Action::SelectWord, - mouse::click::Kind::Triple => Action::SelectLine, - }; + match update { + Update::Click(click) => { + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(click.position()) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; - state.focus = Some(Focus::now()); - state.last_click = Some(click); - state.drag_click = Some(click.kind()); + state.focus = Some(Focus::now()); + state.last_click = Some(click); + state.drag_click = Some(click.kind()); - shell.publish(on_edit(action)); - } - Update::Drag(position) => { - shell.publish(on_edit(Action::Drag(position))); - } - Update::Release => { - state.drag_click = None; - } - Update::Scroll(lines) => { - let bounds = self.content.0.borrow().editor.bounds(); - - if bounds.height >= i32::MAX as f32 { - return; + shell.publish(on_edit(action)); } + Update::Drag(position) => { + shell.publish(on_edit(Action::Drag(position))); + } + Update::Release => { + state.drag_click = None; + } + Update::Scroll(lines) => { + let bounds = self.content.0.borrow().editor.bounds(); - let lines = lines + state.partial_scroll; - state.partial_scroll = lines.fract(); + if bounds.height >= i32::MAX as f32 { + return; + } - shell.publish(on_edit(Action::Scroll { - lines: lines as i32, - })); - } - Update::Binding(binding) => { - fn apply_binding< - H: text::Highlighter, - R: text::Renderer, - Message, - >( - binding: Binding, - content: &Content, - state: &mut State, - on_edit: &dyn Fn(Action) -> Message, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) { - let mut publish = |action| shell.publish(on_edit(action)); + let lines = lines + state.partial_scroll; + state.partial_scroll = lines.fract(); - match binding { - Binding::Unfocus => { - state.focus = None; - state.drag_click = None; - } - Binding::Copy => { - if let Some(selection) = content.selection() { - clipboard.write( - clipboard::Kind::Standard, - selection, - ); + shell.publish(on_edit(Action::Scroll { + lines: lines as i32, + })); + } + Update::Binding(binding) => { + fn apply_binding< + H: text::Highlighter, + R: text::Renderer, + Message, + >( + binding: Binding, + content: &Content, + state: &mut State, + on_edit: &dyn Fn(Action) -> Message, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + let mut publish = + |action| shell.publish(on_edit(action)); + + match binding { + Binding::Unfocus => { + state.focus = None; + state.drag_click = None; } - } - Binding::Cut => { - if let Some(selection) = content.selection() { - clipboard.write( - clipboard::Kind::Standard, - selection, - ); + Binding::Copy => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + } + } + Binding::Cut => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + publish(Action::Edit(Edit::Delete)); + } + } + Binding::Paste => { + if let Some(contents) = + clipboard.read(clipboard::Kind::Standard) + { + publish(Action::Edit(Edit::Paste( + Arc::new(contents), + ))); + } + } + Binding::Move(motion) => { + publish(Action::Move(motion)); + } + Binding::Select(motion) => { + publish(Action::Select(motion)); + } + Binding::SelectWord => { + publish(Action::SelectWord); + } + Binding::SelectLine => { + publish(Action::SelectLine); + } + Binding::SelectAll => { + publish(Action::SelectAll); + } + Binding::Insert(c) => { + publish(Action::Edit(Edit::Insert(c))); + } + Binding::Enter => { + publish(Action::Edit(Edit::Enter)); + } + Binding::Backspace => { + publish(Action::Edit(Edit::Backspace)); + } + Binding::Delete => { publish(Action::Edit(Edit::Delete)); } - } - Binding::Paste => { - if let Some(contents) = - clipboard.read(clipboard::Kind::Standard) - { - publish(Action::Edit(Edit::Paste(Arc::new( - contents, - )))); + Binding::Sequence(sequence) => { + for binding in sequence { + apply_binding( + binding, content, state, on_edit, + clipboard, shell, + ); + } } - } - Binding::Move(motion) => { - publish(Action::Move(motion)); - } - Binding::Select(motion) => { - publish(Action::Select(motion)); - } - Binding::SelectWord => { - publish(Action::SelectWord); - } - Binding::SelectLine => { - publish(Action::SelectLine); - } - Binding::SelectAll => { - publish(Action::SelectAll); - } - Binding::Insert(c) => { - publish(Action::Edit(Edit::Insert(c))); - } - Binding::Enter => { - publish(Action::Edit(Edit::Enter)); - } - Binding::Backspace => { - publish(Action::Edit(Edit::Backspace)); - } - Binding::Delete => { - publish(Action::Edit(Edit::Delete)); - } - Binding::Sequence(sequence) => { - for binding in sequence { - apply_binding( - binding, content, state, on_edit, - clipboard, shell, - ); + Binding::Custom(message) => { + shell.publish(message); } } - Binding::Custom(message) => { - shell.publish(message); - } } - } - apply_binding( - binding, - self.content, - state, - on_edit, - clipboard, - shell, - ); + apply_binding( + binding, + self.content, + state, + on_edit, + clipboard, + shell, + ); - if let Some(focus) = &mut state.focus { - focus.updated_at = Instant::now(); + if let Some(focus) = &mut state.focus { + focus.updated_at = Instant::now(); + } } } } - shell.capture_event(); + let status = { + let is_disabled = self.on_edit.is_none(); + let is_hovered = cursor.is_over(layout.bounds()); + + if is_disabled { + Status::Disabled + } else if state.focus.is_some() { + Status::Focused { is_hovered } + } else if is_hovered { + Status::Hovered + } else { + Status::Active + } + }; + + if is_redraw { + self.last_status = Some(status); + } else if self + .last_status + .is_some_and(|last_status| status != last_status) + { + shell.request_redraw(); + } } fn draw( @@ -807,7 +837,7 @@ where theme: &Theme, _defaults: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -823,20 +853,8 @@ where |highlight| (self.highlighter_format)(highlight, theme), ); - let is_disabled = self.on_edit.is_none(); - let is_mouse_over = cursor.is_over(bounds); - - let status = if is_disabled { - Status::Disabled - } else if state.focus.is_some() { - Status::Focused - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Active)); renderer.fill_quad( renderer::Quad { @@ -1035,7 +1053,7 @@ impl Binding { status, } = event; - if status != Status::Focused { + if !matches!(status, Status::Focused { .. }) { return None; } @@ -1175,7 +1193,9 @@ impl Update { .. }) => { let status = if state.focus.is_some() { - Status::Focused + Status::Focused { + is_hovered: cursor.is_over(bounds), + } } else { Status::Active }; @@ -1221,7 +1241,10 @@ pub enum Status { /// The [`TextEditor`] is being hovered. Hovered, /// The [`TextEditor`] is focused. - Focused, + Focused { + /// Whether the [`TextEditor`] is hovered, while focused. + is_hovered: bool, + }, /// The [`TextEditor`] cannot be interacted with. Disabled, } @@ -1296,7 +1319,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, ..active }, - Status::Focused => Style { + Status::Focused { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border From fec75221f9e7e19f9ad9a00de0fde6f205c2d92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Nov 2024 23:26:09 +0100 Subject: [PATCH 412/657] Fix `text_editor` capturing mouse release events --- widget/src/text_editor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 3bd4c7f9..b6e291e4 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -662,8 +662,6 @@ where cursor, self.key_binding.as_deref(), ) { - shell.capture_event(); - match update { Update::Click(click) => { let action = match click.kind() { @@ -679,6 +677,7 @@ where state.drag_click = Some(click.kind()); shell.publish(on_edit(action)); + shell.capture_event(); } Update::Drag(position) => { shell.publish(on_edit(Action::Drag(position))); @@ -699,6 +698,7 @@ where shell.publish(on_edit(Action::Scroll { lines: lines as i32, })); + shell.capture_event(); } Update::Binding(binding) => { fn apply_binding< @@ -801,6 +801,8 @@ where if let Some(focus) = &mut state.focus { focus.updated_at = Instant::now(); } + + shell.capture_event(); } } } From 03bffe3db61b51d9e28f42c5bfea421b5612c484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 4 Nov 2024 23:29:37 +0100 Subject: [PATCH 413/657] Fix `pick_list` not requesting a redraw when open --- widget/src/pick_list.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 32f859da..6708e7cd 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -518,12 +518,16 @@ where _ => {} }; - let status = if state.is_open { - Status::Opened - } else if cursor.is_over(layout.bounds()) { - Status::Hovered - } else { - Status::Active + let status = { + let is_hovered = cursor.is_over(layout.bounds()); + + if state.is_open { + Status::Opened { is_hovered } + } else if is_hovered { + Status::Hovered + } else { + Status::Active + } }; if let Event::Window(window::Event::RedrawRequested(_now)) = event { @@ -824,7 +828,10 @@ pub enum Status { /// The [`PickList`] is being hovered. Hovered, /// The [`PickList`] is open. - Opened, + Opened { + /// Whether the [`PickList`] is hovered, while open. + is_hovered: bool, + }, } /// The appearance of a pick list. @@ -898,7 +905,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { match status { Status::Active => active, - Status::Hovered | Status::Opened => Style { + Status::Hovered | Status::Opened { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border From e5f1e31a5c068fe992cab076661cb6e2d120bdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 6 Nov 2024 00:02:46 +0100 Subject: [PATCH 414/657] Rename `Overlay::on_event` to `update` --- core/src/overlay.rs | 2 +- core/src/overlay/element.rs | 8 ++++---- core/src/overlay/group.rs | 4 ++-- examples/toast/src/main.rs | 2 +- runtime/src/overlay/nested.rs | 4 ++-- runtime/src/user_interface.rs | 2 +- widget/src/lazy.rs | 4 ++-- widget/src/lazy/component.rs | 4 ++-- widget/src/lazy/responsive.rs | 4 ++-- widget/src/overlay/menu.rs | 2 +- widget/src/pane_grid.rs | 2 +- widget/src/pane_grid/content.rs | 4 ++-- widget/src/pane_grid/title_bar.rs | 2 +- widget/src/themer.rs | 7 +++---- 14 files changed, 25 insertions(+), 26 deletions(-) diff --git a/core/src/overlay.rs b/core/src/overlay.rs index e063bb94..383663af 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -56,7 +56,7 @@ where /// * a [`Clipboard`], if available /// /// By default, it does nothing. - fn on_event( + fn update( &mut self, _event: Event, _layout: Layout<'_>, diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 4a242213..ed870feb 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -49,7 +49,7 @@ where } /// Processes a runtime [`Event`]. - pub fn on_event( + pub fn update( &mut self, event: Event, layout: Layout<'_>, @@ -59,7 +59,7 @@ where shell: &mut Shell<'_, Message>, ) { self.overlay - .on_event(event, layout, cursor, renderer, clipboard, shell); + .update(event, layout, cursor, renderer, clipboard, shell); } /// Returns the current [`mouse::Interaction`] of the [`Element`]. @@ -148,7 +148,7 @@ where self.content.operate(layout, renderer, operation); } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, @@ -160,7 +160,7 @@ where let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); - self.content.on_event( + self.content.update( event, layout, cursor, diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 11ebd579..2b374252 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -72,7 +72,7 @@ where ) } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, @@ -82,7 +82,7 @@ where shell: &mut Shell<'_, Message>, ) { for (child, layout) in self.children.iter_mut().zip(layout.children()) { - child.on_event( + child.update( event.clone(), layout, cursor, diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 893c52e2..8d1e3924 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -489,7 +489,7 @@ mod toast { .translate(Vector::new(self.position.x, self.position.y)) } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 45f6b220..342ad70c 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -158,7 +158,7 @@ where } /// Processes a runtime [`Event`]. - pub fn on_event( + pub fn update( &mut self, event: Event, layout: Layout<'_>, @@ -211,7 +211,7 @@ where }) .unwrap_or_default(); - element.on_event( + element.update( event, layout, if nested_is_over { diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 6997caf4..b2826f71 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -210,7 +210,7 @@ where for event in events.iter().cloned() { let mut shell = Shell::new(messages); - overlay.on_event( + overlay.update( event, Layout::new(&layout), cursor, diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 0b4e3cad..07b90c93 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -386,7 +386,7 @@ where .unwrap_or_default() } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, @@ -396,7 +396,7 @@ where shell: &mut Shell<'_, Message>, ) { let _ = self.with_overlay_mut_maybe(|overlay| { - overlay.on_event(event, layout, cursor, renderer, clipboard, shell); + overlay.update(event, layout, cursor, renderer, clipboard, shell); }); } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 6f661ef6..1758b963 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -601,7 +601,7 @@ where .unwrap_or_default() } - fn on_event( + fn update( &mut self, event: core::Event, layout: Layout<'_>, @@ -614,7 +614,7 @@ where let mut local_shell = Shell::new(&mut local_messages); let _ = self.with_overlay_mut_maybe(|overlay| { - overlay.on_event( + overlay.update( event, layout, cursor, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index f9bd0334..2aef1fa3 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -417,7 +417,7 @@ where .unwrap_or_default() } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, @@ -429,7 +429,7 @@ where let mut is_layout_invalid = false; let _ = self.with_overlay_mut_maybe(|overlay| { - overlay.on_event(event, layout, cursor, renderer, clipboard, shell); + overlay.update(event, layout, cursor, renderer, clipboard, shell); is_layout_invalid = shell.is_layout_invalid(); }); diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b0c0bbad..7907ef01 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -262,7 +262,7 @@ where }) } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 691b0a93..9b87a2d3 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -674,7 +674,7 @@ where { let is_picked = picked_pane == Some(pane); - content.on_event( + content.update( tree, event.clone(), layout, diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index f1f67023..fa9f7a9f 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -239,7 +239,7 @@ where ); } - pub(crate) fn on_event( + pub(crate) fn update( &mut self, tree: &mut Tree, event: Event, @@ -254,7 +254,7 @@ where let body_layout = if let Some(title_bar) = &mut self.title_bar { let mut children = layout.children(); - title_bar.on_event( + title_bar.update( &mut tree.children[1], event.clone(), children.next().unwrap(), diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 618eb4c5..3f4a651e 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -427,7 +427,7 @@ where } } - pub(crate) fn on_event( + pub(crate) fn update( &mut self, tree: &mut Tree, event: Event, diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 41d2aeae..82160f24 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -218,7 +218,7 @@ where ); } - fn on_event( + fn update( &mut self, event: Event, layout: Layout<'_>, @@ -227,9 +227,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) { - self.content.on_event( - event, layout, cursor, renderer, clipboard, shell, - ); + self.content + .update(event, layout, cursor, renderer, clipboard, shell); } fn operate( From 9511bfb971e8bb7e10f95f7d40d5e23c9197e5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 6 Nov 2024 22:03:00 +0100 Subject: [PATCH 415/657] Fix event capturing order in `pane_grid` --- widget/src/pane_grid.rs | 79 +++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 9b87a2d3..7b2956f3 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -491,6 +491,36 @@ where &None }; + let picked_pane = action.picked_pane().map(|(pane, _)| pane); + + for (((pane, content), tree), layout) in self + .panes + .iter() + .copied() + .zip(&mut self.contents) + .zip(&mut tree.children) + .zip(layout.children()) + .filter(|(((pane, _), _), _)| { + self.internal + .maximized() + .map_or(true, |maximized| *pane == maximized) + }) + { + let is_picked = picked_pane == Some(pane); + + content.update( + tree, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + is_picked, + ); + } + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -601,10 +631,6 @@ where } } } - - shell.capture_event(); - } else if action.picked_split().is_some() { - shell.capture_event(); } *action = state::Action::Idle; @@ -657,49 +683,26 @@ where _ => {} } - let picked_pane = action.picked_pane().map(|(pane, _)| pane); - - for (((pane, content), tree), layout) in self - .panes - .iter() - .copied() - .zip(&mut self.contents) - .zip(&mut tree.children) - .zip(layout.children()) - .filter(|(((pane, _), _), _)| { - self.internal - .maximized() - .map_or(true, |maximized| *pane == maximized) - }) - { - let is_picked = picked_pane == Some(pane); - - content.update( - tree, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - is_picked, - ); - } - if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { let interaction = self .grid_interaction(action, layout, cursor) .or_else(|| { - self.contents.iter().zip(layout.children()).find_map( - |(content, layout)| { + self.panes + .iter() + .zip(&self.contents) + .zip(layout.children()) + .filter(|((&pane, _content), _layout)| { + self.internal + .maximized() + .map_or(true, |maximized| pane == maximized) + }) + .find_map(|((_pane, content), layout)| { content.grid_interaction( layout, cursor, on_drag.is_some(), ) - }, - ) + }) }) .unwrap_or(mouse::Interaction::None); From 28ec6df8f0ebf96966bee61caf5a325695314b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 8 Nov 2024 18:07:11 +0100 Subject: [PATCH 416/657] Fix cross-axis compression in `layout::flex` --- core/src/layout/flex.rs | 53 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index ac80d393..2cff5bfd 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -79,6 +79,7 @@ where let max_cross = axis.cross(limits.max()); let mut fill_main_sum = 0; + let mut some_fill_cross = false; let (mut cross, cross_compress) = match axis { Axis::Vertical if width == Length::Shrink => (0.0, true), Axis::Horizontal if height == Length::Shrink => (0.0, true), @@ -90,6 +91,10 @@ where let mut nodes: Vec = Vec::with_capacity(items.len()); nodes.resize(items.len(), Node::default()); + // FIRST PASS + // We lay out non-fluid elements in the main axis. + // If we need to compress the cross axis, then we skip any of these elements + // that are also fluid in the cross axis. 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(); @@ -121,6 +126,41 @@ where nodes[i] = layout; } else { fill_main_sum += fill_main_factor; + some_fill_cross = some_fill_cross || fill_cross_factor != 0; + } + } + + // SECOND PASS (conditional) + // If we must compress the cross axis and there are fluid elements in the + // cross axis, we lay out any of these elements that are also non-fluid in + // the main axis (i.e. the ones we deliberately skipped in the first pass). + // + // We use the maximum cross length obtained in the first pass as the maximum + // cross limit. + 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 size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; + + if fill_main_factor == 0 && fill_cross_factor != 0 { + let (max_width, max_height) = axis.pack(available, cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = + child.as_widget().layout(tree, renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + cross = cross.max(axis.cross(size)); + + nodes[i] = layout; + } } } @@ -135,6 +175,9 @@ where }, }; + // THIRD PASS + // We only have the elements that are fluid in the main axis left. + // We use the remaining space to evenly allocate space based on fill factors. for (i, (child, tree)) in items.iter().zip(trees).enumerate() { let (fill_main_factor, fill_cross_factor) = { let size = child.as_widget().size(); @@ -142,10 +185,16 @@ where axis.pack(size.width.fill_factor(), size.height.fill_factor()) }; - if fill_main_factor != 0 || (cross_compress && fill_cross_factor != 0) { + if fill_main_factor != 0 { let max_main = remaining * fill_main_factor as f32 / fill_main_sum as f32; + let max_main = if max_main.is_nan() { + f32::INFINITY + } else { + max_main + }; + let min_main = if max_main.is_infinite() { 0.0 } else { @@ -178,6 +227,8 @@ where let pad = axis.pack(padding.left, padding.top); let mut main = pad.0; + // FOURTH 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 { main += spacing; From ed2e223fe0f62540947945ea0aa56d0daf3e3f76 Mon Sep 17 00:00:00 2001 From: edwloef Date: Mon, 11 Nov 2024 13:04:37 +0100 Subject: [PATCH 417/657] Fix docs of `Scrollable::with_direction` and `Scrollable::direction` --- widget/src/scrollable.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 528d63c1..24ff1c16 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -95,7 +95,7 @@ where Self::with_direction(content, Direction::default()) } - /// Creates a new vertical [`Scrollable`]. + /// Creates a new [`Scrollable`] with the given [`Direction`]. pub fn with_direction( content: impl Into>, direction: impl Into, @@ -136,7 +136,7 @@ where self } - /// Creates a new [`Scrollable`] with the given [`Direction`]. + /// Sets the [`Direction`] of the [`Scrollable`]. pub fn direction(mut self, direction: impl Into) -> Self { self.direction = direction.into(); self.validate() From 3fc57b7d95f2cd1d8c7bef06547c55195d4e032a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 21 Nov 2024 16:26:17 -0800 Subject: [PATCH 418/657] Remove `surface` argument of `Compositor::screenshot` This argument was completely ignored by the wgpu renderer, and used only for the `clip_mask` by the `tiny_skia` renderer. I believe creating a new clip mask is correct. This way it's possible to render offscreen without needing a surface. --- graphics/src/compositor.rs | 2 -- renderer/src/fallback.rs | 41 +++++++++++++----------------- tiny_skia/src/window/compositor.rs | 9 ++++--- wgpu/src/window/compositor.rs | 1 - winit/src/program.rs | 1 - 5 files changed, 22 insertions(+), 32 deletions(-) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 3026bead..0b862bdb 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -90,7 +90,6 @@ pub trait Compositor: Sized { fn screenshot>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -201,7 +200,6 @@ impl Compositor for () { fn screenshot>( &mut self, _renderer: &mut Self::Renderer, - _surface: &mut Self::Surface, _viewport: &Viewport, _background_color: Color, _overlay: &[T], diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 8cb18bde..52b8317f 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -353,34 +353,27 @@ where fn screenshot>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, viewport: &graphics::Viewport, background_color: Color, overlay: &[T], ) -> Vec { - match (self, renderer, surface) { - ( - Self::Primary(compositor), - Renderer::Primary(renderer), - Surface::Primary(surface), - ) => compositor.screenshot( - renderer, - surface, - viewport, - background_color, - overlay, - ), - ( - Self::Secondary(compositor), - Renderer::Secondary(renderer), - Surface::Secondary(surface), - ) => compositor.screenshot( - renderer, - surface, - viewport, - background_color, - overlay, - ), + match (self, renderer) { + (Self::Primary(compositor), Renderer::Primary(renderer)) => { + compositor.screenshot( + renderer, + viewport, + background_color, + overlay, + ) + } + (Self::Secondary(compositor), Renderer::Secondary(renderer)) => { + compositor.screenshot( + renderer, + viewport, + background_color, + overlay, + ) + } _ => unreachable!(), } } diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 153af6d5..6c144be0 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -121,12 +121,11 @@ impl crate::graphics::Compositor for Compositor { fn screenshot>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Vec { - screenshot(renderer, surface, viewport, background_color, overlay) + screenshot(renderer, viewport, background_color, overlay) } } @@ -212,7 +211,6 @@ pub fn present>( pub fn screenshot>( renderer: &mut Renderer, - surface: &mut Surface, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -222,6 +220,9 @@ pub fn screenshot>( let mut offscreen_buffer: Vec = vec![0; size.width as usize * size.height as usize]; + let mut clip_mask = tiny_skia::Mask::new(size.width, size.height) + .expect("Create clip mask"); + renderer.draw( &mut tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut offscreen_buffer), @@ -229,7 +230,7 @@ pub fn screenshot>( size.height, ) .expect("Create offscreen pixel map"), - &mut surface.clip_mask, + &mut clip_mask, viewport, &[Rectangle::with_size(Size::new( size.width as f32, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 56f33b50..4fe689cf 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -370,7 +370,6 @@ impl graphics::Compositor for Compositor { fn screenshot>( &mut self, renderer: &mut Self::Renderer, - _surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], diff --git a/winit/src/program.rs b/winit/src/program.rs index 130bf220..13873edd 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1456,7 +1456,6 @@ fn run_action( if let Some(window) = window_manager.get_mut(id) { let bytes = compositor.screenshot( &mut window.renderer, - &mut window.surface, window.state.viewport(), window.state.background_color(), &debug.overlay(), From 6d50c62bc79644ee260eb9ced497ddfd688667c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 22 Nov 2024 01:51:00 +0100 Subject: [PATCH 419/657] Honor clones of `task::Handle` with `abort_on_drop` --- runtime/src/task.rs | 63 ++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 4554c74b..22cfb63e 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -9,6 +9,7 @@ use crate::futures::{boxed_stream, BoxStream, MaybeSend}; use crate::Action; use std::future::Future; +use std::sync::Arc; /// A set of concurrent actions to be performed by the iced runtime. /// @@ -183,16 +184,16 @@ impl Task { ( Self(Some(boxed_stream(stream))), Handle { - raw: Some(handle), - abort_on_drop: false, + internal: InternalHandle::Manual(handle), }, ) } None => ( Self(None), Handle { - raw: None, - abort_on_drop: false, + internal: InternalHandle::Manual( + stream::AbortHandle::new_pair().0, + ), }, ), } @@ -220,44 +221,64 @@ impl Task { /// A handle to a [`Task`] that can be used for aborting it. #[derive(Debug, Clone)] pub struct Handle { - raw: Option, - abort_on_drop: bool, + internal: InternalHandle, +} + +#[derive(Debug, Clone)] +enum InternalHandle { + Manual(stream::AbortHandle), + AbortOnDrop(Arc), +} + +impl InternalHandle { + pub fn as_ref(&self) -> &stream::AbortHandle { + match self { + InternalHandle::Manual(handle) => handle, + InternalHandle::AbortOnDrop(handle) => handle.as_ref(), + } + } } impl Handle { /// Aborts the [`Task`] of this [`Handle`]. pub fn abort(&self) { - if let Some(handle) = &self.raw { - handle.abort(); - } + self.internal.as_ref().abort(); } /// Returns a new [`Handle`] that will call [`Handle::abort`] whenever - /// it is dropped. + /// all of its instances are dropped. + /// + /// If a [`Handle`] is cloned, [`Handle::abort`] will only be called + /// once all of the clones are dropped. /// /// This can be really useful if you do not want to worry about calling /// [`Handle::abort`] yourself. - pub fn abort_on_drop(mut self) -> Self { - Self { - raw: self.raw.take(), - abort_on_drop: true, + pub fn abort_on_drop(self) -> Self { + match &self.internal { + InternalHandle::Manual(handle) => Self { + internal: InternalHandle::AbortOnDrop(Arc::new(handle.clone())), + }, + InternalHandle::AbortOnDrop(_) => self, } } /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted. pub fn is_aborted(&self) -> bool { - if let Some(handle) = &self.raw { - handle.is_aborted() - } else { - true - } + self.internal.as_ref().is_aborted() } } impl Drop for Handle { fn drop(&mut self) { - if self.abort_on_drop { - self.abort(); + if let InternalHandle::AbortOnDrop(handle) = &mut self.internal { + let handle = std::mem::replace( + handle, + Arc::new(stream::AbortHandle::new_pair().0), + ); + + if let Some(handle) = Arc::into_inner(handle) { + handle.abort(); + } } } } From 6ccc828607b22a0583c12bad638ee2bc7e7310b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 22 Nov 2024 02:14:33 +0100 Subject: [PATCH 420/657] Use `Task::run` in `download_progress` example --- examples/download_progress/src/download.rs | 19 ++---- examples/download_progress/src/main.rs | 68 +++++++++++++--------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index a8e7b404..d63fb906 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -1,24 +1,13 @@ use iced::futures::{SinkExt, Stream, StreamExt}; use iced::stream::try_channel; -use iced::Subscription; -use std::hash::Hash; use std::sync::Arc; -// Just a little utility function -pub fn file( - id: I, - url: T, -) -> iced::Subscription<(I, Result)> { - Subscription::run_with_id( - id, - download(url.to_string()).map(move |progress| (id, progress)), - ) -} - -fn download(url: String) -> impl Stream> { +pub fn download( + url: impl AsRef, +) -> impl Stream> { try_channel(1, move |mut output| async move { - let response = reqwest::get(&url).await?; + let response = reqwest::get(url.as_ref()).await?; let total = response.content_length().ok_or(Error::NoContentLength)?; let _ = output.send(Progress::Downloading { percent: 0.0 }).await; diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index bcc01606..f4b07203 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,7 +1,10 @@ mod download; +use download::download; + +use iced::task; use iced::widget::{button, center, column, progress_bar, text, Column}; -use iced::{Center, Element, Right, Subscription}; +use iced::{Center, Element, Right, Task}; pub fn main() -> iced::Result { iced::application( @@ -9,7 +12,6 @@ pub fn main() -> iced::Result { Example::update, Example::view, ) - .subscription(Example::subscription) .run() } @@ -23,7 +25,7 @@ struct Example { pub enum Message { Add, Download(usize), - DownloadProgressed((usize, Result)), + DownloadProgressed(usize, Result), } impl Example { @@ -34,32 +36,38 @@ impl Example { } } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Task { match message { Message::Add => { self.last_id += 1; self.downloads.push(Download::new(self.last_id)); + + Task::none() } Message::Download(index) => { - if let Some(download) = self.downloads.get_mut(index) { - download.start(); - } + let Some(download) = self.downloads.get_mut(index) else { + return Task::none(); + }; + + let task = download.start(); + + task.map(move |progress| { + Message::DownloadProgressed(index, progress) + }) } - Message::DownloadProgressed((id, progress)) => { + Message::DownloadProgressed(id, progress) => { if let Some(download) = self.downloads.iter_mut().find(|download| download.id == id) { download.progress(progress); } + + Task::none() } } } - fn subscription(&self) -> Subscription { - Subscription::batch(self.downloads.iter().map(Download::subscription)) - } - fn view(&self) -> Element { let downloads = Column::with_children(self.downloads.iter().map(Download::view)) @@ -90,7 +98,7 @@ struct Download { #[derive(Debug)] enum State { Idle, - Downloading { progress: f32 }, + Downloading { progress: f32, _task: task::Handle }, Finished, Errored, } @@ -103,14 +111,28 @@ impl Download { } } - pub fn start(&mut self) { + pub fn start( + &mut self, + ) -> Task> { match self.state { State::Idle { .. } | State::Finished { .. } | State::Errored { .. } => { - self.state = State::Downloading { progress: 0.0 }; + let (task, handle) = Task::stream(download( + "https://huggingface.co/\ + mattshumer/Reflection-Llama-3.1-70B/\ + resolve/main/model-00001-of-00162.safetensors", + )) + .abortable(); + + self.state = State::Downloading { + progress: 0.0, + _task: handle.abort_on_drop(), + }; + + task } - State::Downloading { .. } => {} + State::Downloading { .. } => Task::none(), } } @@ -118,7 +140,7 @@ impl Download { &mut self, new_progress: Result, ) { - if let State::Downloading { progress } = &mut self.state { + if let State::Downloading { progress, .. } = &mut self.state { match new_progress { Ok(download::Progress::Downloading { percent }) => { *progress = percent; @@ -133,20 +155,10 @@ impl Download { } } - pub fn subscription(&self) -> Subscription { - match self.state { - State::Downloading { .. } => { - download::file(self.id, "https://huggingface.co/mattshumer/Reflection-Llama-3.1-70B/resolve/main/model-00001-of-00162.safetensors") - .map(Message::DownloadProgressed) - } - _ => Subscription::none(), - } - } - pub fn view(&self) -> Element { let current_progress = match &self.state { State::Idle { .. } => 0.0, - State::Downloading { progress } => *progress, + State::Downloading { progress, .. } => *progress, State::Finished { .. } => 100.0, State::Errored { .. } => 0.0, }; From 5be1d545d0baa09b82fe380d9246f66474cce302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 22 Nov 2024 04:06:52 +0100 Subject: [PATCH 421/657] Implement `pin` widget --- examples/layout/src/main.rs | 24 +++- widget/src/helpers.rs | 36 ++++- widget/src/lib.rs | 3 + widget/src/pin.rs | 272 ++++++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 widget/src/pin.rs diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 4280a003..d71cbcbc 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -3,7 +3,8 @@ use iced::keyboard; use iced::mouse; use iced::widget::{ button, canvas, center, checkbox, column, container, horizontal_rule, - horizontal_space, pick_list, row, scrollable, text, vertical_rule, + horizontal_space, pick_list, pin, row, scrollable, stack, text, + vertical_rule, }; use iced::{ color, Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, @@ -151,6 +152,10 @@ impl Example { title: "Quotes", view: quotes, }, + Self { + title: "Pinning", + view: pinning, + }, ]; fn is_first(self) -> bool { @@ -309,6 +314,23 @@ fn quotes<'a>() -> Element<'a, Message> { .into() } +fn pinning<'a>() -> Element<'a, Message> { + column![ + "The pin widget can be used to position a widget \ + at some fixed coordinates inside some other widget.", + stack![ + container(pin("• (50, 50)").width(Fill).height(Fill).x(50).y(50)) + .width(500) + .height(500) + .style(container::bordered_box), + pin("• (300, 300)").width(Fill).height(Fill).x(300).y(300), + ] + ] + .align_x(Center) + .spacing(10) + .into() +} + fn square<'a>(size: impl Into + Copy) -> Element<'a, Message> { struct Square; diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 33dff647..a669b290 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -24,7 +24,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Stack, Themer}; +use crate::{Column, MouseArea, Pin, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -249,6 +249,40 @@ where container(content).center(Length::Fill) } +/// Creates a new [`Pin`] widget with the given content. +/// +/// A [`Pin`] widget positions its contents at some fixed coordinates inside of its boundaries. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::pin; +/// use iced::Fill; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pin("This text is displayed at coordinates (50, 50)!") +/// .width(Fill) +/// .height(Fill) +/// .x(50) +/// .y(50) +/// .into() +/// } +/// ``` +pub fn pin<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Pin::new(content) +} + /// Creates a new [`Column`] with the given children. /// /// Columns distribute their children vertically. diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 776a04a0..38c9929a 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -11,6 +11,7 @@ pub use iced_runtime::core; mod action; mod column; mod mouse_area; +mod pin; mod row; mod space; mod stack; @@ -63,6 +64,8 @@ pub use pane_grid::PaneGrid; #[doc(no_inline)] pub use pick_list::PickList; #[doc(no_inline)] +pub use pin::Pin; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; diff --git a/widget/src/pin.rs b/widget/src/pin.rs new file mode 100644 index 00000000..7d561894 --- /dev/null +++ b/widget/src/pin.rs @@ -0,0 +1,272 @@ +//! A pin widget positions a widget at some fixed coordinates inside its boundaries. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::pin; +//! use iced::Fill; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! pin("This text is displayed at coordinates (50, 50)!") +//! .width(Fill) +//! .height(Fill) +//! .x(50) +//! .y(50) +//! .into() +//! } +//! ``` +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::{ + self, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, + Shell, Size, Vector, Widget, +}; + +/// A widget that positions its contents at some fixed coordinates inside of its boundaries. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::pin; +/// use iced::Fill; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pin("This text is displayed at coordinates (50, 50)!") +/// .width(Fill) +/// .height(Fill) +/// .x(50) +/// .y(50) +/// .into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +where + Renderer: core::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + width: Length, + height: Length, + position: Point, +} + +impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + /// Creates a [`Pin`] widget with the given content. + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + width: Length::Shrink, + height: Length::Shrink, + position: Point::ORIGIN, + } + } + + /// Sets the width of the [`Pin`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Pin`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the position of the [`Pin`]; where the pinned widget will be displayed. + pub fn position(mut self, position: impl Into) -> Self { + self.position = position.into(); + self + } + + /// Sets the X coordinate of the [`Pin`]. + pub fn x(mut self, x: impl Into) -> Self { + self.position.x = x.into().0; + self + } + + /// Sets the Y coordinate of the [`Pin`]. + pub fn y(mut self, y: impl Into) -> Self { + self.position.y = y.into().0; + self + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> widget::tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> widget::tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut widget::Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + let available = + limits.max() - Size::new(self.position.x, self.position.y); + + let node = self + .content + .as_widget() + .layout(tree, renderer, &layout::Limits::new(Size::ZERO, available)) + .move_to(self.position); + + let size = limits.resolve(self.width, self.height, node.size()); + layout::Node::with_children(size, vec![node]) + } + + fn operate( + &self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + tree, + layout.children().next().unwrap(), + renderer, + operation, + ); + } + + fn update( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + tree, + event, + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + fn mouse_interaction( + &self, + tree: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + tree, + layout.children().next().unwrap(), + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if let Some(clipped_viewport) = bounds.intersection(viewport) { + self.content.as_widget().draw( + tree, + renderer, + theme, + style, + layout.children().next().unwrap(), + cursor, + &clipped_viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + tree, + layout.children().next().unwrap(), + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from( + pin: Pin<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(pin) + } +} From a805177b250b2316c0a9b4de06a3be4556d6944a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 22 Nov 2024 04:13:38 +0100 Subject: [PATCH 422/657] Make `pin` widget `Fill` parent by default --- examples/layout/src/main.rs | 4 ++-- widget/src/helpers.rs | 2 -- widget/src/pin.rs | 10 ++++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index d71cbcbc..e83a1f7d 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -319,11 +319,11 @@ fn pinning<'a>() -> Element<'a, Message> { "The pin widget can be used to position a widget \ at some fixed coordinates inside some other widget.", stack![ - container(pin("• (50, 50)").width(Fill).height(Fill).x(50).y(50)) + container(pin("• (50, 50)").x(50).y(50)) .width(500) .height(500) .style(container::bordered_box), - pin("• (300, 300)").width(Fill).height(Fill).x(300).y(300), + pin("• (300, 300)").x(300).y(300), ] ] .align_x(Center) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index a669b290..3d85ba70 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -267,8 +267,6 @@ where /// /// fn view(state: &State) -> Element<'_, Message> { /// pin("This text is displayed at coordinates (50, 50)!") -/// .width(Fill) -/// .height(Fill) /// .x(50) /// .y(50) /// .into() diff --git a/widget/src/pin.rs b/widget/src/pin.rs index 7d561894..1f167716 100644 --- a/widget/src/pin.rs +++ b/widget/src/pin.rs @@ -14,8 +14,6 @@ //! //! fn view(state: &State) -> Element<'_, Message> { //! pin("This text is displayed at coordinates (50, 50)!") -//! .width(Fill) -//! .height(Fill) //! .x(50) //! .y(50) //! .into() @@ -33,6 +31,8 @@ use crate::core::{ /// A widget that positions its contents at some fixed coordinates inside of its boundaries. /// +/// By default, a [`Pin`] widget will try to fill its parent. +/// /// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } @@ -47,8 +47,6 @@ use crate::core::{ /// /// fn view(state: &State) -> Element<'_, Message> { /// pin("This text is displayed at coordinates (50, 50)!") -/// .width(Fill) -/// .height(Fill) /// .x(50) /// .y(50) /// .into() @@ -75,8 +73,8 @@ where ) -> Self { Self { content: content.into(), - width: Length::Shrink, - height: Length::Shrink, + width: Length::Fill, + height: Length::Fill, position: Point::ORIGIN, } } From 0a39f5eac7987495de81f19c6c83258227312554 Mon Sep 17 00:00:00 2001 From: Chris Manning Date: Sun, 24 Nov 2024 15:04:52 +0000 Subject: [PATCH 423/657] Request redraw in tooltip widget when cursor is hovering --- widget/src/tooltip.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index e66f5e4a..3b9d2066 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -213,6 +213,8 @@ where if was_idle != is_idle { shell.invalidate_layout(); + } else if self.position == Position::FollowCursor && *state != State::Idle { + shell.request_redraw(); } self.content.as_widget_mut().update( From 602661372c921ef5079283ccd5f477c63977239f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 2 Dec 2024 19:53:16 +0100 Subject: [PATCH 424/657] Fix new `clippy` lints --- .cargo/config.toml | 1 + core/src/element.rs | 4 ++-- core/src/overlay/element.rs | 10 +++++----- core/src/overlay/group.rs | 10 +++++----- core/src/text.rs | 4 ++-- core/src/widget/operation.rs | 4 ++-- core/src/widget/text.rs | 4 ++-- examples/bezier_tool/src/main.rs | 2 +- examples/custom_quad/src/main.rs | 2 +- examples/custom_widget/src/main.rs | 4 ++-- examples/geometry/src/main.rs | 2 +- examples/loading_spinners/src/circular.rs | 2 +- examples/loading_spinners/src/linear.rs | 2 +- examples/loupe/src/main.rs | 2 +- examples/toast/src/main.rs | 6 +++--- graphics/src/geometry/stroke.rs | 4 ++-- widget/src/button.rs | 2 +- widget/src/checkbox.rs | 4 ++-- widget/src/column.rs | 6 +++--- widget/src/combo_box.rs | 4 ++-- widget/src/container.rs | 6 +++--- widget/src/helpers.rs | 8 ++++---- widget/src/keyed/column.rs | 6 +++--- widget/src/lazy.rs | 10 +++++----- widget/src/lazy/component.rs | 20 ++++++++++---------- widget/src/lazy/responsive.rs | 12 ++++++------ widget/src/mouse_area.rs | 4 ++-- widget/src/overlay/menu.rs | 8 ++++---- widget/src/pane_grid.rs | 4 ++-- widget/src/pane_grid/content.rs | 6 +++--- widget/src/pane_grid/title_bar.rs | 2 +- widget/src/pin.rs | 4 ++-- widget/src/progress_bar.rs | 4 ++-- widget/src/qr_code.rs | 2 +- widget/src/radio.rs | 4 ++-- widget/src/row.rs | 10 +++++----- widget/src/rule.rs | 4 ++-- widget/src/scrollable.rs | 4 ++-- widget/src/slider.rs | 4 ++-- widget/src/stack.rs | 2 +- widget/src/svg.rs | 6 +++--- widget/src/text/rich.rs | 4 ++-- widget/src/text_editor.rs | 4 ++-- widget/src/text_input.rs | 4 ++-- widget/src/themer.rs | 8 ++++---- widget/src/toggler.rs | 4 ++-- widget/src/tooltip.rs | 8 ++++---- widget/src/vertical_slider.rs | 4 ++-- 48 files changed, 123 insertions(+), 122 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 49ca3252..df979396 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,3 @@ [alias] lint = "clippy --workspace --benches --all-features --no-deps -- -D warnings" +lint-fix = "clippy --fix --allow-dirty --workspace --benches --all-features --no-deps -- -D warnings" diff --git a/core/src/element.rs b/core/src/element.rs index 03e56b43..82ba753b 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -394,8 +394,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget - for Explain<'a, Message, Theme, Renderer> +impl Widget + for Explain<'_, Message, Theme, Renderer> where Renderer: crate::Renderer, { diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index ed870feb..7a179663 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -130,8 +130,8 @@ impl<'a, A, B, Theme, Renderer> Map<'a, A, B, Theme, Renderer> { } } -impl<'a, A, B, Theme, Renderer> Overlay - for Map<'a, A, B, Theme, Renderer> +impl Overlay + for Map<'_, A, B, Theme, Renderer> where Renderer: crate::Renderer, { @@ -203,11 +203,11 @@ where self.content.is_over(layout, renderer, cursor_position) } - fn overlay<'b>( - &'b mut self, + fn overlay<'a>( + &'a mut self, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { + ) -> Option> { self.content .overlay(layout, renderer) .map(|overlay| overlay.map(self.mapper)) diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 2b374252..e07744e3 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -57,8 +57,8 @@ where } } -impl<'a, Message, Theme, Renderer> Overlay - for Group<'a, Message, Theme, Renderer> +impl Overlay + for Group<'_, Message, Theme, Renderer> where Renderer: crate::Renderer, { @@ -152,11 +152,11 @@ where }) } - fn overlay<'b>( - &'b mut self, + fn overlay<'a>( + &'a mut self, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { + ) -> Option> { let children = self .children .iter_mut() diff --git a/core/src/text.rs b/core/src/text.rs index a9e3dce5..c144fd24 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -446,7 +446,7 @@ impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> { } } -impl<'a, Link, Font: PartialEq> PartialEq for Span<'a, Link, Font> { +impl PartialEq for Span<'_, Link, Font> { fn eq(&self, other: &Self) -> bool { self.text == other.text && self.size == other.size @@ -474,7 +474,7 @@ impl<'a> IntoFragment<'a> for Fragment<'a> { } } -impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { +impl<'a> IntoFragment<'a> for &'a Fragment<'_> { fn into_fragment(self) -> Fragment<'a> { Fragment::Borrowed(self) } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 097c3601..6bdb27f6 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -138,7 +138,7 @@ where operation: &'a mut dyn Operation, } - impl<'a, T, O> Operation for BlackBox<'a, T> { + impl Operation for BlackBox<'_, T> { fn container( &mut self, id: Option<&Id>, @@ -218,7 +218,7 @@ where operation: &'a mut dyn Operation, } - impl<'a, A, B> Operation for MapRef<'a, A> { + impl Operation for MapRef<'_, A> { fn container( &mut self, id: Option<&Id>, diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index b34c5632..d3d1cffd 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -206,8 +206,8 @@ where #[derive(Debug, Default)] pub struct State(pub paragraph::Plain

); -impl<'a, Message, Theme, Renderer> Widget - for Text<'a, Theme, Renderer> +impl Widget + for Text<'_, Theme, Renderer> where Theme: Catalog, Renderer: text::Renderer, diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 5e4da0c2..e8f0efc9 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -88,7 +88,7 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl canvas::Program for Bezier<'_> { type State = Option; fn update( diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index dc425cc6..f9c07da9 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -75,7 +75,7 @@ mod quad { } } - impl<'a, Message> From for Element<'a, Message> { + impl From for Element<'_, Message> { fn from(circle: CustomQuad) -> Self { Self::new(circle) } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 58f3c54a..d561c2e0 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -62,8 +62,8 @@ mod circle { } } - impl<'a, Message, Theme, Renderer> From - for Element<'a, Message, Theme, Renderer> + impl From + for Element<'_, Message, Theme, Renderer> where Renderer: renderer::Renderer, { diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 3c7969c5..d53ae6a5 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -145,7 +145,7 @@ mod rainbow { } } - impl<'a, Message> From for Element<'a, Message> { + impl From for Element<'_, Message> { fn from(rainbow: Rainbow) -> Self { Self::new(rainbow) } diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index a10d5cec..33232fac 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -88,7 +88,7 @@ where } } -impl<'a, Theme> Default for Circular<'a, Theme> +impl Default for Circular<'_, Theme> where Theme: StyleSheet, { diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 91c8d523..a10b64f0 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -70,7 +70,7 @@ where } } -impl<'a, Theme> Default for Linear<'a, Theme> +impl Default for Linear<'_, Theme> where Theme: StyleSheet, { diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 1c748d42..6b7d053a 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -74,7 +74,7 @@ mod loupe { content: Element<'a, Message>, } - impl<'a, Message> Widget for Loupe<'a, Message> { + impl Widget for Loupe<'_, Message> { fn tag(&self) -> widget::tree::Tag { self.content.as_widget().tag() } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 8d1e3924..a1b5886f 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -281,7 +281,7 @@ mod toast { } } - impl<'a, Message> Widget for Manager<'a, Message> { + impl Widget for Manager<'_, Message> { fn size(&self) -> Size { self.content.as_widget().size() } @@ -464,8 +464,8 @@ mod toast { timeout_secs: u64, } - impl<'a, 'b, Message> overlay::Overlay - for Overlay<'a, 'b, Message> + impl overlay::Overlay + for Overlay<'_, '_, Message> { fn layout( &mut self, diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index b8f4515e..88a5fd7b 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -23,7 +23,7 @@ pub struct Stroke<'a> { pub line_dash: LineDash<'a>, } -impl<'a> Stroke<'a> { +impl Stroke<'_> { /// Sets the color of the [`Stroke`]. pub fn with_color(self, color: Color) -> Self { Stroke { @@ -48,7 +48,7 @@ impl<'a> Stroke<'a> { } } -impl<'a> Default for Stroke<'a> { +impl Default for Stroke<'_> { fn default() -> Self { Stroke { style: Style::Solid(Color::BLACK), diff --git a/widget/src/button.rs b/widget/src/button.rs index 9eac2e4c..d1fa9302 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -89,7 +89,7 @@ enum OnPress<'a, Message> { Closure(Box Message + 'a>), } -impl<'a, Message: Clone> OnPress<'a, Message> { +impl OnPress<'_, Message> { fn get(&self) -> Message { match self { OnPress::Direct(message) => message.clone(), diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 625dee7c..3686d34c 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -247,8 +247,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget - for Checkbox<'a, Message, Theme, Renderer> +impl Widget + for Checkbox<'_, Message, Theme, Renderer> where Renderer: text::Renderer, Theme: Catalog, diff --git a/widget/src/column.rs b/widget/src/column.rs index a3efab94..c729cbdb 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -173,7 +173,7 @@ where } } -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> +impl Default for Column<'_, Message, Renderer> where Renderer: crate::core::Renderer, { @@ -195,8 +195,8 @@ impl<'a, Message, Theme, Renderer: crate::core::Renderer> } } -impl<'a, Message, Theme, Renderer> Widget - for Column<'a, Message, Theme, Renderer> +impl Widget + for Column<'_, Message, Theme, Renderer> where Renderer: crate::core::Renderer, { diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 8b8e895d..500d2bec 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -459,8 +459,8 @@ enum TextInputEvent { TextChanged(String), } -impl<'a, T, Message, Theme, Renderer> Widget - for ComboBox<'a, T, Message, Theme, Renderer> +impl Widget + for ComboBox<'_, T, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone, diff --git a/widget/src/container.rs b/widget/src/container.rs index b7b2b39e..d9740f72 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -228,8 +228,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget - for Container<'a, Message, Theme, Renderer> +impl Widget + for Container<'_, Message, Theme, Renderer> where Theme: Catalog, Renderer: core::Renderer, @@ -650,7 +650,7 @@ pub trait Catalog { /// A styling function for a [`Container`]. pub type StyleFn<'a, Theme> = Box Style + 'a>; -impl<'a, Theme> From