diff --git a/Cargo.lock b/Cargo.lock index 3bc71b0ab8..5287021b00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -904,6 +904,13 @@ dependencies = [ "iced", ] +[[package]] +name = "component" +version = "0.1.0" +dependencies = [ + "iced", +] + [[package]] name = "concurrent-queue" version = "2.5.0" diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 28c74c11da..94f83bfd67 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -55,7 +55,7 @@ pub trait Renderer { /// Creates an [`image::Allocation`] for the given [`image::Handle`] and calls the given callback with it. fn allocate_image( - &mut self, + &self, handle: &image::Handle, callback: impl FnOnce(Result) + Send + 'static, ); diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 1167cae878..ab5b307db7 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -17,7 +17,7 @@ impl Renderer for () { fn fill_quad(&mut self, _quad: renderer::Quad, _background: impl Into) {} fn allocate_image( - &mut self, + &self, handle: &image::Handle, callback: impl FnOnce(Result) + Send + 'static, ) { diff --git a/core/src/shell.rs b/core/src/shell.rs index ef1b369ace..b0201162cc 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -83,6 +83,12 @@ impl<'a, Message> Shell<'a, Message> { self.bus.push(message) } + /// Forwards the given `Message` and fulfills the given [`Receipt`] + /// once processed. + pub fn forward(&mut self, message: Message, receipt: Receipt) { + self.bus.forward(message, receipt); + } + /// Marks the current event as captured. Prevents "event bubbling". /// /// A widget should capture an event when no ancestor should @@ -280,7 +286,7 @@ pub enum Diff { /// A channel of messages published by a [`Shell`]. #[derive(Debug)] pub struct Bus { - messages: Vec<(T, Rc<()>)>, + messages: Vec<(T, Receipt)>, } impl Bus { @@ -306,17 +312,22 @@ impl Bus { /// The returned [`Tracking`] can be used to determine if the message /// was processed. pub fn push(&mut self, message: T) -> Tracking { - let receipt = Rc::new(()); - let tracking = Tracking(Rc::downgrade(&receipt)); + let receipt = Receipt::new(); + let tracking = receipt.tracking(); self.messages.push((message, receipt)); tracking } + /// Forward a new message to the [`Bus`] with the given [`Receipt`]. + pub fn forward(&mut self, message: T, receipt: Receipt) { + self.messages.push((message, receipt)); + } + /// Drains the [`Bus`]. - pub fn drain(&mut self) -> impl Iterator { - self.messages.drain(..).map(|(message, _receipt)| message) + pub fn drain(&mut self) -> impl Iterator { + self.messages.drain(..) } } @@ -339,7 +350,7 @@ impl IntoIterator for Bus { /// An iterator returned by the implementation of [`IntoIterator`] for [`Bus`]. pub struct IntoIter { - iter: vec::IntoIter<(T, Rc<()>)>, + iter: vec::IntoIter<(T, Receipt)>, } impl Iterator for IntoIter { @@ -350,6 +361,20 @@ impl Iterator for IntoIter { } } +/// Proof that a message has been received. +#[derive(Debug)] +pub struct Receipt(Rc<()>); + +impl Receipt { + fn new() -> Self { + Self(Rc::new(())) + } + + fn tracking(&self) -> Tracking { + Tracking(Rc::downgrade(&self.0)) + } +} + /// A message tracking returned by [`Shell::publish`]. #[derive(Debug, Clone)] pub struct Tracking(rc::Weak<()>); diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml new file mode 100644 index 0000000000..1f76febbda --- /dev/null +++ b/examples/component/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "component" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2024" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug"] diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs new file mode 100644 index 0000000000..f77a07ede2 --- /dev/null +++ b/examples/component/src/main.rs @@ -0,0 +1,130 @@ +use iced::Element; +use iced::widget::center; + +use numeric_input::numeric_input; + +pub fn main() -> iced::Result { + iced::run(Example::update, Example::view) +} + +#[derive(Default)] +struct Example { + value: Option, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + NumericInputChanged(Option), +} + +impl Example { + 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::{Component, button, component, row, text, text_input}; + use iced::{Center, Element, Fill, Renderer}; + + pub struct NumericInput { + value: Option, + on_change: Box) -> Message>, + } + + pub fn numeric_input( + value: Option, + on_change: impl Fn(Option) -> Message + 'static, + ) -> NumericInput { + NumericInput::new(value, on_change) + } + + #[derive(Debug, Clone)] + pub enum Event { + InputChanged(String), + IncrementPressed, + DecrementPressed, + } + + impl NumericInput { + pub fn new( + value: Option, + on_change: impl Fn(Option) -> Message + 'static, + ) -> Self { + Self { + value, + on_change: Box::new(on_change), + } + } + } + + impl<'a, Message> Component<'a, Message> for NumericInput { + type State = (); + type Event = Event; + + fn update( + &mut self, + _state: &mut Self::State, + event: Event, + _renderer: &Renderer, + ) -> Option { + 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<'a, Event> { + 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(i32::to_string).unwrap_or_default(), + ) + .on_input(Event::InputChanged) + .padding(10), + button("+", Event::IncrementPressed), + ] + .align_y(Center) + .spacing(10) + .into() + } + } + + impl<'a, Message> From> for Element<'a, Message> + where + Message: 'a, + { + fn from(numeric_input: NumericInput) -> Self { + component(numeric_input) + } + } +} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 287c56c59b..831b6f9ecf 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -63,7 +63,7 @@ where } fn allocate_image( - &mut self, + &self, handle: &image::Handle, callback: impl FnOnce(Result) + Send + 'static, ) { diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 24615eb71e..3772c20284 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -184,7 +184,7 @@ where /// cache = user_interface.into_cache(); /// /// // Process the produced messages - /// for message in messages.drain() { + /// for (message, _receipt) in messages.drain() { /// counter.update(message); /// } /// } @@ -487,7 +487,7 @@ where /// /// cache = user_interface.into_cache(); /// - /// for message in messages.drain() { + /// for (message, _receipt) in messages.drain() { /// counter.update(message); /// } /// diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index f1659b2c11..3d8f539e1b 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -209,7 +209,7 @@ impl core::Renderer for Renderer { } fn allocate_image( - &mut self, + &self, _handle: &core::image::Handle, callback: impl FnOnce(Result) + Send + 'static, ) { diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b1552fcb6b..76d9ec9ea1 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -685,13 +685,13 @@ impl core::Renderer for Renderer { } fn allocate_image( - &mut self, + &self, _handle: &core::image::Handle, _callback: impl FnOnce(Result) + Send + 'static, ) { #[cfg(feature = "image")] self.image_cache - .get_mut() + .borrow_mut() .allocate_image(_handle, _callback); } diff --git a/widget/src/action.rs b/widget/src/action.rs index 2ee7b6c556..8884d94881 100644 --- a/widget/src/action.rs +++ b/widget/src/action.rs @@ -11,7 +11,8 @@ pub struct Action { } impl Action { - fn new() -> Self { + /// Creates an [`Action`] that does nothing. + pub fn none() -> Self { Self { message_to_publish: None, redraw_request: window::RedrawRequest::Wait, @@ -27,7 +28,7 @@ impl Action { pub fn capture() -> Self { Self { event_status: event::Status::Captured, - ..Self::new() + ..Self::none() } } @@ -38,7 +39,7 @@ impl Action { pub fn publish(message: Message) -> Self { Self { message_to_publish: Some(message), - ..Self::new() + ..Self::none() } } @@ -47,7 +48,7 @@ impl Action { pub fn request_redraw() -> Self { Self { redraw_request: window::RedrawRequest::NextFrame, - ..Self::new() + ..Self::none() } } @@ -59,7 +60,7 @@ impl Action { pub fn request_redraw_at(at: Instant) -> Self { Self { redraw_request: window::RedrawRequest::At(at), - ..Self::new() + ..Self::none() } } @@ -69,6 +70,12 @@ impl Action { self } + /// Requests a redraw at the given [`Instant`]. See [`Self::request_redraw_at`]. + pub fn and_request_redraw_at(mut self, at: Instant) -> Self { + self.redraw_request = window::RedrawRequest::At(at); + self + } + /// Converts the [`Action`] into its internal parts. /// /// This method is meant to be used by runtimes, libraries, or internal diff --git a/widget/src/component.rs b/widget/src/component.rs new file mode 100644 index 0000000000..7385155410 --- /dev/null +++ b/widget/src/component.rs @@ -0,0 +1,556 @@ +//! Build and reuse custom widgets using The Elm Architecture. +use crate::Action; +use crate::core::event; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::shell; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; +use crate::core::{self, Element, Event, Length, Point, Rectangle, Shell, Size, Vector, Widget}; + +/// A reusable, custom widget that uses The Elm Architecture. +/// +/// A [`Component`] allows you to implement custom widgets as if they were +/// `iced` applications with encapsulated state. +/// +/// In other words, a [`Component`] allows you to turn `iced` applications into +/// custom widgets and embed them without cumbersome wiring. +/// +/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) +/// and update the internal state of the [`Component`]. +/// +/// 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]. +pub trait Component<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { + /// The internal state of this [`Component`]. + type State: Default + 'static; + + /// The type of event this [`Component`] handles internally. + type Event: 'static; + + /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. + /// + /// It can produce a `Message` for the parent application. + fn update( + &mut self, + state: &mut Self::State, + event: Self::Event, + renderer: &Renderer, + ) -> Option; + + /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) + /// on user interaction. + fn view(&self, state: &Self::State) -> Element<'a, Self::Event, Theme, Renderer>; + + /// Listens to a runtime [`Event`] and performs an [`Action`] as a result. + /// + /// If the [`Action`] publishes a [`Component::Event`], it will be immediately fed + /// to [`update`](Self::update). + /// + /// By default, it returns [`Action::none`]. + fn listen( + &self, + _state: &Self::State, + _event: &Event, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Action { + Action::none() + } + + /// Returns the current [`mouse::Interaction`] of the [`Component`]. + /// + /// This interaction will override any interaction produced by the [`view`](Self::view) + /// of the [`Component`]. + /// + /// By default, it returns [`mouse::Interaction::None`]. + fn mouse_interaction(&self, _state: &Self::State) -> mouse::Interaction { + mouse::Interaction::None + } + + /// Reconciles the current [`Component`] with its internal [`State`](Self::State) persisted + /// in the widget tree. + /// + /// This method will be called every time the widget tree changes. You can leverage it to + /// detect and react to changes in the [`Component`]. + /// + /// By default, it does nothing. + fn diff(&mut self, _state: &mut Self::State) {} + + /// Run the provided [`widget::Operation`] on the [`Component`]. + /// + /// By default, it does nothing. + fn operate( + &self, + _state: &Self::State, + _bounds: Rectangle, + _operation: &mut dyn widget::Operation, + ) { + } +} + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn component<'a, C, Message, Theme, Renderer>( + component: C, +) -> Element<'a, Message, Theme, Renderer> +where + C: Component<'a, Message, Theme, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + Element::new(Instance { + component, + view: crate::space().into(), + limits: layout::Limits::new(Size::ZERO, Size::INFINITE), + layout: layout::Node::new(Size::ZERO), + is_outdated: true, + has_overlay: false, + }) +} + +struct Instance<'a, C, Message, Theme, Renderer> +where + C: Component<'a, Message, Theme, Renderer> + 'a, +{ + component: C, + view: Element<'a, C::Event, Theme, Renderer>, + limits: layout::Limits, + layout: layout::Node, + is_outdated: bool, + has_overlay: bool, +} + +struct Internal { + state: State, + events: shell::Bus, +} + +impl<'a, C, Message, Theme, Renderer> Widget + for Instance<'a, C, Message, Theme, Renderer> +where + C: Component<'a, Message, Theme, Renderer> + 'a, + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(Internal { + state: C::State::default(), + events: shell::Bus::::new(), + }) + } + + fn diff(&mut self, tree: &mut Tree) { + let internal = tree.state.downcast_mut::>(); + + self.component.diff(&mut internal.state); + + if self.is_outdated { + self.view = self.component.view(&internal.state); + tree.diff_children(std::slice::from_mut(&mut self.view)); + + self.is_outdated = false; + } + } + + fn size(&self) -> Size { + self.view.as_widget().size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if &self.limits != limits { + self.limits = *limits; + self.layout = self + .view + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits); + } + + layout::Node::new(self.layout.size()) + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let internal = tree.state.downcast_mut::>(); + + let action = self + .component + .listen(&internal.state, event, layout.bounds(), cursor); + + let (publish, redraw_request, event_status) = action.into_inner(); + + shell.request_redraw_at(redraw_request); + + if let event::Status::Captured = event_status { + shell.capture_event(); + } + + if let Some(event) = publish { + let _ = internal.events.push(event); + } + + if !shell.is_event_captured() { + let mut local_shell = shell.local(&mut internal.events); + + self.view.as_widget_mut().update( + &mut tree.children[0], + event, + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + cursor, + renderer, + &mut local_shell, + viewport, + ); + + if local_shell.is_event_captured() { + shell.capture_event(); + } + + if let Some(diff) = local_shell.is_layout_invalid() { + shell.invalidate_layout_with(diff); + } + + if local_shell.are_widgets_invalid() { + shell.invalidate_widgets(); + } + + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); + shell.clipboard_mut().merge(local_shell.clipboard_mut()); + } + + if internal.events.is_empty() { + return; + } + + for (event, receipt) in internal.events.drain() { + if let Some(message) = self.component.update(&mut internal.state, event, renderer) { + shell.forward(message, receipt); + } + } + + let previous_sizing = self.view.as_widget().size(); + + self.view = self.component.view(&internal.state); + tree.diff_children(std::slice::from_mut(&mut self.view)); + + let previous_size = self.layout.size(); + self.layout = + self.view + .as_widget_mut() + .layout(&mut tree.children[0], renderer, &self.limits); + + let new_sizing = self.view.as_widget().size(); + + // We must invalidate application layout in 3 instances: + // + // 1. The size hint of the component changes. Other widgets + // may change layout behavior. + // + // 2. The size hint of the component is `Shrink` for any axis + // and the component has changed size. The new size may + // push other widgets around. + // + // 3. The overlay status of the component changes. The + // runtime will only call `overlay` again if the layout + // is invalidated. + if new_sizing != previous_sizing { + shell.invalidate_widgets(); + } else if (new_sizing.width == Length::Shrink || new_sizing.height == Length::Shrink) + && previous_size != self.layout.size() + { + shell.invalidate_layout(); + } else { + let has_overlay = self + .view + .as_widget_mut() + .overlay( + &mut tree.children[0], + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + renderer, + viewport, + Vector::ZERO, + ) + .is_some(); + + if self.has_overlay != has_overlay { + self.has_overlay = has_overlay; + shell.invalidate_layout(); + } + } + + self.is_outdated = false; + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + let internal = tree.state.downcast_mut::>(); + + let mut local_shell = shell.local(&mut internal.events); + + self.view.as_widget_mut().update( + &mut tree.children[0], + event, + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + cursor, + renderer, + &mut local_shell, + viewport, + ); + + if internal.events.is_empty() { + return; + } + } + + shell.request_redraw(); + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.view.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + cursor, + viewport, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let internal = tree.state.downcast_ref::>(); + + let interaction = self.component.mouse_interaction(&internal.state); + + if interaction != mouse::Interaction::None { + return interaction; + } + + self.view.as_widget().mouse_interaction( + &tree.children[0], + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + cursor, + viewport, + renderer, + ) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let internal = tree.state.downcast_ref::>(); + + self.component + .operate(&internal.state, layout.bounds(), operation); + + self.view.as_widget_mut().operate( + &mut tree.children[0], + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + renderer, + operation, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + let overlay = self.view.as_widget_mut().overlay( + &mut tree.children[0], + Layout::with_offset(layout.position() - Point::ORIGIN, &self.layout), + renderer, + viewport, + translation, + )?; + + self.has_overlay = true; + + Some(overlay::Element::new(Box::new(Overlay { + component: &mut self.component, + internal: tree.state.downcast_mut(), + raw: overlay, + is_outdated: &mut self.is_outdated, + }))) + } +} + +struct Overlay<'a, 'b, C, Message, Theme, Renderer> +where + C: Component<'a, Message, Theme, Renderer>, +{ + component: &'b mut C, + internal: &'b mut Internal, + is_outdated: &'b mut bool, + raw: overlay::Element<'b, C::Event, Theme, Renderer>, +} + +impl<'a, 'b, C, Message, Theme, Renderer> overlay::Overlay + for Overlay<'a, 'b, C, Message, Theme, Renderer> +where + C: Component<'a, Message, Theme, Renderer>, + Renderer: core::Renderer, +{ + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { + self.raw.as_overlay_mut().layout(renderer, bounds) + } + + fn update( + &mut self, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + shell: &mut Shell<'_, Message>, + ) { + let mut local_shell = shell.local(&mut self.internal.events); + + self.raw + .as_overlay_mut() + .update(event, layout, cursor, renderer, &mut local_shell); + + if local_shell.is_event_captured() { + shell.capture_event(); + } + + if let Some(diff) = local_shell.is_layout_invalid() { + shell.invalidate_layout_with(diff); + } + + if local_shell.are_widgets_invalid() { + shell.invalidate_widgets(); + } + + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); + shell.clipboard_mut().merge(local_shell.clipboard_mut()); + + if self.internal.events.is_empty() { + return; + } + + for (event, receipt) in self.internal.events.drain() { + if let Some(message) = self + .component + .update(&mut self.internal.state, event, renderer) + { + shell.forward(message, receipt); + } + } + + *self.is_outdated = true; + + shell.invalidate_layout(); + shell.request_redraw(); + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self.raw + .as_overlay() + .draw(renderer, theme, style, layout, cursor); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + ) -> mouse::Interaction { + self.raw + .as_overlay() + .mouse_interaction(layout, cursor, renderer) + } + + fn index(&self) -> f32 { + self.raw.as_overlay().index() + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.raw + .as_overlay_mut() + .operate(layout, renderer, operation); + } + + fn overlay<'c>( + &'c mut self, + layout: Layout<'c>, + renderer: &Renderer, + ) -> Option> { + let overlay = self.raw.as_overlay_mut().overlay(layout, renderer)?; + + Some(overlay::Element::new(Box::new(Overlay { + component: self.component, + raw: overlay, + internal: self.internal, + is_outdated: self.is_outdated, + }))) + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 18fbb0084f..23194a854e 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -30,6 +30,7 @@ use crate::{Column, Grid, MouseArea, Pin, Responsive, Row, Sensor, Space, Stack, use std::borrow::Borrow; use std::ops::RangeInclusive; +pub use crate::component::component; pub use crate::table::table; /// Creates a [`Column`] with the given children. diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 7a78f11672..495a32a463 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -1,10 +1,4 @@ #![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)] -pub(crate) mod helpers; - -pub mod component; - -#[allow(deprecated)] -pub use component::Component; mod cache; @@ -23,6 +17,20 @@ use std::cell::RefCell; use std::hash::{Hash, Hasher as H}; use std::rc::Rc; +/// 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")] +pub fn lazy<'a, Message, Theme, Renderer, Dependency, View>( + dependency: Dependency, + view: impl Fn(&Dependency) -> View + 'a, +) -> Lazy<'a, Message, Theme, Renderer, Dependency, View> +where + Dependency: Hash + 'a, + View: Into>, +{ + Lazy::new(dependency, view) +} + /// A widget that only rebuilds its contents when necessary. #[cfg(feature = "lazy")] pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> { diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs deleted file mode 100644 index dd0317e642..0000000000 --- a/widget/src/lazy/component.rs +++ /dev/null @@ -1,625 +0,0 @@ -//! Build and reuse custom widgets using The Elm Architecture. -#![allow(deprecated)] -use crate::core::layout::{self, Layout}; -use crate::core::mouse; -use crate::core::overlay; -use crate::core::renderer; -use crate::core::shell; -use crate::core::widget; -use crate::core::widget::tree::{self, Tree}; -use crate::core::{self, Element, Length, Rectangle, Shell, Size, Vector, Widget}; - -use ouroboros::self_referencing; -use std::cell::RefCell; -use std::marker::PhantomData; -use std::rc::Rc; - -/// A reusable, custom widget that uses The Elm Architecture. -/// -/// A [`Component`] allows you to implement custom widgets as if they were -/// `iced` applications with encapsulated state. -/// -/// In other words, a [`Component`] allows you to turn `iced` applications into -/// custom widgets and embed them without cumbersome wiring. -/// -/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) -/// and update the internal state of the [`Component`]. -/// -/// 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", - 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 { - /// The internal state of this [`Component`]. - type State: Default; - - /// The type of event this [`Component`] handles internally. - type Event; - - /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. - /// - /// It can produce a `Message` for the parent application. - fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option; - - /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) - /// on user interaction. - fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Theme, Renderer>; - - /// Update the [`Component`] state based on the provided [`Operation`](widget::Operation) - /// - /// By default, it does nothing. - fn operate( - &self, - _bounds: Rectangle, - _state: &mut Self::State, - _operation: &mut dyn widget::Operation, - ) { - } - - /// Returns a [`Size`] hint for laying out the [`Component`]. - /// - /// This hint may be used by some widget containers to adjust their sizing strategy - /// during construction. - fn size_hint(&self) -> Size { - Size { - width: Length::Shrink, - height: Length::Shrink, - } - } -} - -struct Tag(T); - -/// Turns an implementor of [`Component`] into an [`Element`] that can be -/// embedded in any application. -pub fn view<'a, C, Message, Theme, Renderer>(component: C) -> Element<'a, Message, Theme, Renderer> -where - C: Component + 'a, - C::State: 'static, - Message: 'a, - Theme: 'a, - Renderer: core::Renderer + 'a, -{ - Element::new(Instance { - state: RefCell::new(Some( - StateBuilder { - component: Box::new(component), - message: PhantomData, - state: PhantomData, - element_builder: |_| None, - } - .build(), - )), - tree: RefCell::new(Rc::new(RefCell::new(None))), - }) -} - -struct Instance<'a, Message, Theme, Renderer, Event, S> { - state: RefCell>>, - tree: RefCell>>>, -} - -#[self_referencing] -struct State<'a, Message: 'a, Theme: 'a, Renderer: 'a, Event: 'a, S: 'a> { - component: Box + 'a>, - message: PhantomData, - state: PhantomData, - - #[borrows(component)] - #[covariant] - element: Option>, -} - -impl Instance<'_, Message, Theme, Renderer, Event, S> -where - S: Default + 'static, - Renderer: renderer::Renderer, -{ - fn diff_self(&self) { - self.with_element_mut(|element| { - self.tree - .borrow_mut() - .borrow_mut() - .as_mut() - .unwrap() - .diff_children(std::slice::from_mut(element)); - }); - } - - fn rebuild_element_if_necessary(&self) { - let inner = self.state.borrow_mut().take().unwrap(); - if inner.borrow_element().is_none() { - let heads = inner.into_heads(); - - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| { - Some( - component.view( - self.tree - .borrow() - .borrow() - .as_ref() - .unwrap() - .state - .downcast_ref::(), - ), - ) - }, - } - .build(), - ); - self.diff_self(); - } else { - *self.state.borrow_mut() = Some(inner); - } - } - - fn rebuild_element_with_operation( - &self, - layout: Layout<'_>, - operation: &mut dyn widget::Operation, - ) { - let heads = self.state.borrow_mut().take().unwrap().into_heads(); - - heads.component.operate( - layout.bounds(), - self.tree - .borrow_mut() - .borrow_mut() - .as_mut() - .unwrap() - .state - .downcast_mut(), - operation, - ); - - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| { - Some( - component.view( - self.tree - .borrow() - .borrow() - .as_ref() - .unwrap() - .state - .downcast_ref(), - ), - ) - }, - } - .build(), - ); - self.diff_self(); - } - - fn with_element(&self, f: impl FnOnce(&Element<'_, Event, Theme, Renderer>) -> T) -> T { - self.with_element_mut(|element| f(element)) - } - - fn with_element_mut( - &self, - f: impl FnOnce(&mut Element<'_, Event, Theme, Renderer>) -> T, - ) -> T { - self.rebuild_element_if_necessary(); - self.state - .borrow_mut() - .as_mut() - .unwrap() - .with_element_mut(|element| f(element.as_mut().unwrap())) - } -} - -impl Widget - for Instance<'_, Message, Theme, Renderer, Event, S> -where - S: 'static + Default, - Renderer: core::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - let state = Rc::new(RefCell::new(Some(Tree { - tag: tree::Tag::of::>(), - state: tree::State::new(S::default()), - children: vec![Tree::empty()], - }))); - - *self.tree.borrow_mut() = state.clone(); - self.diff_self(); - - tree::State::new(state) - } - - fn diff(&mut self, tree: &mut Tree) { - let tree = tree.state.downcast_ref::>>>(); - *self.tree.borrow_mut() = tree.clone(); - self.rebuild_element_if_necessary(); - } - - fn size(&self) -> Size { - self.with_element(|element| element.as_widget().size()) - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let t = tree.state.downcast_mut::>>>(); - - self.with_element_mut(|element| { - element.as_widget_mut().layout( - &mut t.borrow_mut().as_mut().unwrap().children[0], - renderer, - limits, - ) - }) - } - - fn update( - &mut self, - tree: &mut Tree, - event: &core::Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) { - let mut local_messages = shell::Bus::new(); - let mut local_shell = shell.local(&mut local_messages); - - let t = tree.state.downcast_mut::>>>(); - self.with_element_mut(|element| { - element.as_widget_mut().update( - &mut t.borrow_mut().as_mut().unwrap().children[0], - event, - layout, - cursor, - renderer, - &mut local_shell, - viewport, - ); - }); - - if local_shell.is_event_captured() { - shell.capture_event(); - } - - local_shell.revalidate_layout(|diff| shell.invalidate_layout_with(diff)); - shell.request_redraw_at(local_shell.redraw_request()); - shell.request_input_method(local_shell.input_method()); - shell.clipboard_mut().merge(local_shell.clipboard_mut()); - - if !local_messages.is_empty() { - let mut heads = self.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads.component.update( - t.borrow_mut().as_mut().unwrap().state.downcast_mut(), - message, - ) - }) { - shell.publish(message); - } - - self.state = RefCell::new(Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |_| None, - } - .build(), - )); - - shell.invalidate_layout(); - shell.request_redraw(); - } - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.rebuild_element_with_operation(layout, operation); - - let tree = tree.state.downcast_mut::>>>(); - self.with_element_mut(|element| { - element.as_widget_mut().operate( - &mut tree.borrow_mut().as_mut().unwrap().children[0], - layout, - renderer, - operation, - ); - }); - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let tree = tree.state.downcast_ref::>>>(); - self.with_element(|element| { - element.as_widget().draw( - &tree.borrow().as_ref().unwrap().children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - }); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let tree = tree.state.downcast_ref::>>>(); - self.with_element(|element| { - element.as_widget().mouse_interaction( - &tree.borrow().as_ref().unwrap().children[0], - layout, - cursor, - viewport, - renderer, - ) - }) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'b>, - renderer: &Renderer, - viewport: &Rectangle, - translation: Vector, - ) -> Option> { - self.rebuild_element_if_necessary(); - - let state = tree.state.downcast_mut::>>>(); - let tree = state.borrow_mut().take().unwrap(); - - 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, - viewport, - translation, - ) - .map(|overlay| RefCell::new(overlay::Nested::new(overlay))) - }) - }, - } - .build(); - - #[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 - } - } -} - -struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>( - Option>, -); - -impl Drop - for Overlay<'_, '_, Message, Theme, Renderer, Event, S> -{ - fn drop(&mut self) { - if let Some(heads) = self.0.take().map(Inner::into_heads) { - *heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree); - } - } -} - -#[self_referencing] -struct Inner<'a, 'b, Message, Theme, Renderer, Event, S> { - instance: &'a mut Instance<'b, Message, Theme, Renderer, Event, S>, - tree: Tree, - types: PhantomData<(Message, Event, S)>, - - #[borrows(mut instance, mut tree)] - #[not_covariant] - overlay: Option>>, -} - -struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> { - overlay: Option>, -} - -impl - OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S> -{ - fn with_overlay_maybe( - &self, - f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T, - ) -> Option { - self.overlay - .as_ref() - .unwrap() - .0 - .as_ref() - .unwrap() - .with_overlay(|overlay| overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T, - ) -> Option { - self.overlay - .as_mut() - .unwrap() - .0 - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(|nested| (f)(nested.get_mut()))) - } -} - -impl overlay::Overlay - for OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S> -where - Renderer: core::Renderer, - S: 'static + Default, -{ - fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { - self.with_overlay_maybe(|overlay| overlay.layout(renderer, bounds)) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| overlay.mouse_interaction(layout, cursor, renderer)) - .unwrap_or_default() - } - - fn update( - &mut self, - event: &core::Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - shell: &mut Shell<'_, Message>, - ) { - let mut local_messages = shell::Bus::new(); - let mut local_shell = shell.local(&mut local_messages); - - let _ = self.with_overlay_mut_maybe(|overlay| { - overlay.update(event, layout, cursor, renderer, &mut local_shell); - }); - - if local_shell.is_event_captured() { - shell.capture_event(); - } - - local_shell.revalidate_layout(|diff| shell.invalidate_layout_with(diff)); - shell.request_redraw_at(local_shell.redraw_request()); - shell.request_input_method(local_shell.input_method()); - shell.clipboard_mut().merge(local_shell.clipboard_mut()); - - if !local_messages.is_empty() { - let mut inner = self.overlay.take().unwrap().0.take().unwrap().into_heads(); - let mut heads = inner.instance.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(inner.tree.state.downcast_mut(), message) - }) { - shell.publish(message); - } - - *inner.instance.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |_| None, - } - .build(), - ); - - self.overlay = Some(Overlay(Some( - InnerBuilder { - instance: inner.instance, - tree: inner.tree, - types: PhantomData, - overlay_builder: |_, _| None, - } - .build(), - ))); - - shell.invalidate_layout(); - } - } -} diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs deleted file mode 100644 index 7e7601cb94..0000000000 --- a/widget/src/lazy/helpers.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::core::{self, Element}; -use crate::lazy::component; - -use std::hash::Hash; - -#[allow(deprecated)] -pub use crate::lazy::{Component, Lazy}; - -/// 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")] -pub fn lazy<'a, Message, Theme, Renderer, Dependency, View>( - dependency: Dependency, - view: impl Fn(&Dependency) -> View + 'a, -) -> Lazy<'a, Message, Theme, Renderer, Dependency, View> -where - Dependency: Hash + 'a, - View: Into>, -{ - Lazy::new(dependency, view) -} - -/// 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> -where - C: Component + 'a, - C::State: 'static, - Message: 'a, - Theme: 'a, - Renderer: core::Renderer + 'a, -{ - component::view(component) -} diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 7e0465f315..2a82a4eb4d 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -20,6 +20,7 @@ mod themer; pub mod button; pub mod checkbox; pub mod combo_box; +pub mod component; pub mod container; pub mod float; pub mod grid; @@ -52,7 +53,7 @@ pub use helpers::*; mod lazy; #[cfg(feature = "lazy")] -pub use crate::lazy::helpers::*; +pub use crate::lazy::{Lazy, lazy}; #[doc(no_inline)] pub use button::Button; @@ -63,6 +64,8 @@ pub use column::Column; #[doc(no_inline)] pub use combo_box::ComboBox; #[doc(no_inline)] +pub use component::Component; +#[doc(no_inline)] pub use container::Container; #[doc(no_inline)] pub use float::Float; diff --git a/winit/src/lib.rs b/winit/src/lib.rs index f7d2ca1b81..5d6dd579db 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -1243,7 +1243,7 @@ where let mut outputs = Vec::new(); while !messages.is_empty() { - for message in messages.drain() { + for (message, _receipt) in messages.drain() { let task = runtime.enter(|| program.update(message)); if let Some(mut stream) = runtime::task::into_stream(task) {