diff options
author | 2022-08-06 00:32:57 +0200 | |
---|---|---|
committer | 2022-08-06 00:32:57 +0200 | |
commit | 1923dbf7f0769d55e5283f572fde0ce752e28b86 (patch) | |
tree | 7be9b36f941f6e13ddc8884f715c04555b1e77db /native | |
parent | 1b4f38c71f6e05e26599ee75ea9c91dde96e71ae (diff) | |
parent | c23ed7e4a0a2b62a0d7cabe6e35d7323eac543d2 (diff) | |
download | iced-1923dbf7f0769d55e5283f572fde0ce752e28b86.tar.gz iced-1923dbf7f0769d55e5283f572fde0ce752e28b86.tar.bz2 iced-1923dbf7f0769d55e5283f572fde0ce752e28b86.zip |
Merge pull request #1393 from iced-rs/deprecate-stateful-widgets
Replace stateful widgets with the new `iced_pure` API
Diffstat (limited to 'native')
41 files changed, 3015 insertions, 1631 deletions
diff --git a/native/src/command.rs b/native/src/command.rs index 89d0f045..b0b12805 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -3,6 +3,8 @@ mod action; pub use action::Action; +use crate::widget; + use iced_futures::MaybeSend; use std::fmt; @@ -24,6 +26,13 @@ impl<T> Command<T> { Self(iced_futures::Command::single(action)) } + /// Creates a [`Command`] that performs a [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self { + Self(iced_futures::Command::single(Action::Widget( + widget::Action::new(operation), + ))) + } + /// Creates a [`Command`] that performs the action of the given future. pub fn perform<A>( future: impl Future<Output = T> + 'static + MaybeSend, @@ -51,6 +60,7 @@ impl<T> Command<T> { ) -> Command<A> where T: 'static, + A: 'static, { let Command(command) = self; diff --git a/native/src/command/action.rs b/native/src/command/action.rs index 1bb03cef..3fb02899 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -1,5 +1,6 @@ use crate::clipboard; use crate::system; +use crate::widget; use crate::window; use iced_futures::MaybeSend; @@ -23,6 +24,9 @@ pub enum Action<T> { /// Run a system action. System(system::Action<T>), + + /// Run a widget action. + Widget(widget::Action<T>), } impl<T> Action<T> { @@ -34,6 +38,7 @@ impl<T> Action<T> { f: impl Fn(T) -> A + 'static + MaybeSend + Sync, ) -> Action<A> where + A: 'static, T: 'static, { use iced_futures::futures::FutureExt; @@ -43,6 +48,7 @@ impl<T> Action<T> { Self::Clipboard(action) => Action::Clipboard(action.map(f)), Self::Window(window) => Action::Window(window), Self::System(system) => Action::System(system.map(f)), + Self::Widget(widget) => Action::Widget(widget.map(f)), } } } @@ -56,6 +62,7 @@ impl<T> fmt::Debug for Action<T> { } Self::Window(action) => write!(f, "Action::Window({:?})", action), Self::System(action) => write!(f, "Action::System({:?})", action), + Self::Widget(_action) => write!(f, "Action::Widget"), } } } diff --git a/native/src/element.rs b/native/src/element.rs index 425bddc2..8b994d73 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -3,9 +3,11 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::{ - Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, -}; +use crate::widget; +use crate::widget::tree::{self, Tree}; +use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell, Widget}; + +use std::borrow::Borrow; /// A generic [`Widget`]. /// @@ -15,25 +17,33 @@ use crate::{ /// If you have a [built-in widget], you should be able to use `Into<Element>` /// to turn it into an [`Element`]. /// -/// [built-in widget]: widget/index.html#built-in-widgets +/// [built-in widget]: crate::widget #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { - pub(crate) widget: Box<dyn Widget<Message, Renderer> + 'a>, + widget: Box<dyn Widget<Message, Renderer> + 'a>, } -impl<'a, Message, Renderer> Element<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { /// Creates a new [`Element`] containing the given [`Widget`]. - pub fn new( - widget: impl Widget<Message, Renderer> + 'a, - ) -> Element<'a, Message, Renderer> { - Element { + pub fn new(widget: impl Widget<Message, Renderer> + 'a) -> Self + where + Renderer: crate::Renderer, + { + Self { widget: Box::new(widget), } } + /// Returns a reference to the [`Widget`] of the [`Element`], + pub fn as_widget(&self) -> &dyn Widget<Message, Renderer> { + self.widget.as_ref() + } + + /// Returns a mutable reference to the [`Widget`] of the [`Element`], + pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> { + self.widget.as_mut() + } + /// Applies a transformation to the produced message of the [`Element`]. /// /// This method is useful when you want to decouple different parts of your @@ -168,127 +178,22 @@ where /// } /// } /// ``` - pub fn map<F, B>(self, f: F) -> Element<'a, B, Renderer> - where - Message: 'static, - Renderer: 'a, - B: 'static, - F: 'static + Fn(Message) -> B, - { - Element { - widget: Box::new(Map::new(self.widget, f)), - } - } - - /// Marks the [`Element`] as _to-be-explained_. - /// - /// The [`Renderer`] will explain the layout of the [`Element`] graphically. - /// This can be very useful for debugging your layout! - /// - /// [`Renderer`]: crate::Renderer - pub fn explain<C: Into<Color>>( + pub fn map<B>( self, - color: C, - ) -> Element<'a, Message, Renderer> + f: impl Fn(Message) -> B + 'a, + ) -> Element<'a, B, Renderer> where - Message: 'static, - Renderer: 'a, + Message: 'a, + Renderer: crate::Renderer + 'a, + B: 'a, { - Element { - widget: Box::new(Explain::new(self, color.into())), - } - } - - /// Returns the width of the [`Element`]. - pub fn width(&self) -> Length { - self.widget.width() - } - - /// Returns the height of the [`Element`]. - pub fn height(&self) -> Length { - self.widget.height() - } - - /// Computes the layout of the [`Element`] in the given [`Limits`]. - /// - /// [`Limits`]: layout::Limits - pub fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.widget.layout(renderer, limits) - } - - /// Processes a runtime [`Event`]. - pub fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - /// Draws the [`Element`] and its children using the given [`Layout`]. - pub fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.widget.draw( - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } - - /// Returns the current [`mouse::Interaction`] of the [`Element`]. - pub fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.widget.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - /// Returns the overlay of the [`Element`], if there is any. - pub fn overlay<'b>( - &'b mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'b, Message, Renderer>> { - self.widget.overlay(layout, renderer) + Element::new(Map::new(self.widget, f)) } } struct Map<'a, A, B, Renderer> { widget: Box<dyn Widget<A, Renderer> + 'a>, - mapper: Box<dyn Fn(A) -> B>, + mapper: Box<dyn Fn(A) -> B + 'a>, } impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { @@ -297,7 +202,7 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { mapper: F, ) -> Map<'a, A, B, Renderer> where - F: 'static + Fn(A) -> B, + F: 'a + Fn(A) -> B, { Map { widget, @@ -309,9 +214,25 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer> where Renderer: crate::Renderer + 'a, - A: 'static, - B: 'static, + A: 'a, + B: 'a, { + fn tag(&self) -> tree::Tag { + self.widget.tag() + } + + fn state(&self) -> tree::State { + self.widget.state() + } + + fn children(&self) -> Vec<Tree> { + self.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + fn width(&self) -> Length { self.widget.width() } @@ -328,8 +249,45 @@ where self.widget.layout(renderer, limits) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<B>, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation<B>, + } + + impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<T>, + ), + ) { + self.operation.container(id, &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); + } + } + + self.widget + .operate(tree, layout, &mut MapOperation { operation }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -341,6 +299,7 @@ where let mut local_shell = Shell::new(&mut local_messages); let status = self.widget.on_event( + tree, event, layout, cursor_position, @@ -356,6 +315,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -364,6 +324,7 @@ where viewport: &Rectangle, ) { self.widget.draw( + tree, renderer, theme, style, @@ -375,12 +336,14 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.widget.mouse_interaction( + tree, layout, cursor_position, viewport, @@ -388,134 +351,32 @@ where ) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, B, Renderer>> { + ) -> Option<overlay::Element<'b, B, Renderer>> { let mapper = &self.mapper; self.widget - .overlay(layout, renderer) + .overlay(tree, layout, renderer) .map(move |overlay| overlay.map(mapper)) } } -struct Explain<'a, Message, Renderer: crate::Renderer> { - element: Element<'a, Message, Renderer>, - color: Color, -} - -impl<'a, Message, Renderer> Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, +impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a> + for Element<'a, Message, Renderer> { - fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self { - Explain { element, color } + fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) { + self.widget.borrow() } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, +impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a> + for &Element<'a, Message, Renderer> { - fn width(&self) -> Length { - self.element.widget.width() - } - - fn height(&self) -> Length { - self.element.widget.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.element.widget.layout(renderer, limits) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.element.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - fn explain_layout<Renderer: crate::Renderer>( - renderer: &mut Renderer, - color: Color, - layout: Layout<'_>, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border_color: color, - border_width: 1.0, - border_radius: 0.0, - }, - Color::TRANSPARENT, - ); - - for child in layout.children() { - explain_layout(renderer, color, child); - } - } - - self.element.widget.draw( - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - - explain_layout(renderer, self.color, layout); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.element.widget.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.element.overlay(layout, renderer) + fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) { + self.widget.borrow() } } diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 5fbcbca0..94121d76 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -16,8 +16,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::Element; + use crate::layout::{Limits, Node}; -use crate::{Alignment, Element, Padding, Point, Size}; +use crate::{Alignment, Padding, Point, Size}; /// The main axis of a flex layout. #[derive(Debug)] @@ -84,8 +86,8 @@ where items.iter().for_each(|child| { let cross_fill_factor = match axis { - Axis::Horizontal => child.height(), - Axis::Vertical => child.width(), + Axis::Horizontal => child.as_widget().height(), + Axis::Vertical => child.as_widget().width(), } .fill_factor(); @@ -95,7 +97,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -107,8 +109,8 @@ where for (i, child) in items.iter().enumerate() { let fill_factor = match axis { - Axis::Horizontal => child.width(), - Axis::Vertical => child.height(), + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), } .fill_factor(); @@ -130,7 +132,7 @@ where Size::new(max_width, max_height), ); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -149,8 +151,8 @@ where for (i, child) in items.iter().enumerate() { let fill_factor = match axis { - Axis::Horizontal => child.width(), - Axis::Vertical => child.height(), + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), } .fill_factor(); @@ -179,7 +181,7 @@ where Size::new(max_width, max_height), ); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); if align_items != Alignment::Fill { cross = cross.max(axis.cross(layout.size())); diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 792d2905..905d3389 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -10,6 +10,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; +use crate::widget::tree::{self, Tree}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// An interactive component that can be displayed on top of other widgets. @@ -40,6 +42,36 @@ where cursor_position: Point, ); + /// Returns the [`Tag`] of the [`Widget`]. + /// + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec<Tree> { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _layout: Layout<'_>, + _operation: &mut dyn widget::Operation<Message>, + ) { + } + /// Processes a runtime [`Event`]. /// /// It receives: @@ -77,3 +109,26 @@ where mouse::Interaction::Idle } } + +/// Obtains the first overlay [`Element`] found in the given children. +/// +/// This method will generally only be used by advanced users that are +/// implementing the [`Widget`](crate::Widget) trait. +pub fn from_children<'a, Message, Renderer>( + children: &'a [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option<Element<'a, Message, Renderer>> +where + Renderer: crate::Renderer, +{ + children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget().overlay(state, layout, renderer) + }) + .next() +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index de2e1f37..b919c221 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -4,6 +4,7 @@ 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, Vector}; /// A generic [`Overlay`]. @@ -102,6 +103,15 @@ where self.overlay .draw(renderer, theme, style, layout, cursor_position) } + + /// Applies an [`Operation`] to the [`Element`]. + pub fn operate( + &self, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<Message>, + ) { + self.overlay.operate(layout, operation); + } } struct Map<'a, A, B, Renderer> { diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 7b8d4d9e..08135872 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -9,6 +9,7 @@ use crate::text::{self, Text}; use crate::touch; use crate::widget::container::{self, Container}; use crate::widget::scrollable::{self, Scrollable}; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, @@ -114,15 +115,23 @@ where } /// The local state of a [`Menu`]. -#[derive(Debug, Clone, Default)] +#[derive(Debug)] pub struct State { - scrollable: scrollable::State, + tree: Tree, } impl State { /// Creates a new [`State`] for a [`Menu`]. pub fn new() -> Self { - Self::default() + Self { + tree: Tree::empty(), + } + } +} + +impl Default for State { + fn default() -> Self { + Self::new() } } @@ -131,6 +140,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { + state: &'a mut Tree, container: Container<'a, Message, Renderer>, width: u16, target_height: f32, @@ -161,18 +171,20 @@ where style, } = menu; - let container = - Container::new(Scrollable::new(&mut state.scrollable).push(List { - options, - hovered_option, - last_selection, - font, - text_size, - padding, - style, - })); + let container = Container::new(Scrollable::new(List { + options, + hovered_option, + last_selection, + font, + text_size, + padding, + style, + })); + + state.tree.diff(&container as &dyn Widget<_, _>); Self { + state: &mut state.tree, container, width, target_height, @@ -187,6 +199,18 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { + fn tag(&self) -> tree::Tag { + self.container.tag() + } + + fn state(&self) -> tree::State { + self.container.state() + } + + fn children(&self) -> Vec<Tree> { + self.container.children() + } + fn layout( &self, renderer: &Renderer, @@ -230,6 +254,7 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { self.container.on_event( + self.state, event, layout, cursor_position, @@ -247,6 +272,7 @@ where renderer: &Renderer, ) -> mouse::Interaction { self.container.mouse_interaction( + self.state, layout, cursor_position, viewport, @@ -279,6 +305,7 @@ where ); self.container.draw( + self.state, renderer, theme, style, @@ -344,6 +371,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -407,6 +435,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -423,6 +452,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/program.rs b/native/src/program.rs index 9ee72703..c71c237f 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -26,5 +26,5 @@ pub trait Program: Sized { /// Returns the widgets to display in the [`Program`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; + fn view(&self) -> Element<'_, Self::Message, Self::Renderer>; } diff --git a/native/src/renderer.rs b/native/src/renderer.rs index a7305a55..ef64ac36 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -21,7 +21,7 @@ pub trait Renderer: Sized { element: &Element<'a, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - element.layout(self, limits) + element.as_widget().layout(self, limits) } /// Draws the primitives recorded in the given closure in a new layer. diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index ef6f437e..4bcf1e0c 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -4,6 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -22,6 +23,7 @@ use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, base: layout::Node, + state: widget::Tree, overlay: Option<layout::Node>, bounds: Size, } @@ -88,17 +90,21 @@ where pub fn build<E: Into<Element<'a, Message, Renderer>>>( root: E, bounds: Size, - _cache: Cache, + cache: Cache, renderer: &mut Renderer, ) -> Self { let root = root.into(); + let Cache { mut state } = cache; + state.diff(root.as_widget()); + let base = renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)); UserInterface { root, base, + state, overlay: None, bounds, } @@ -182,9 +188,12 @@ where use std::mem::ManuallyDrop; let mut state = State::Updated; - let mut manual_overlay = ManuallyDrop::new( - self.root.overlay(Layout::new(&self.base), renderer), - ); + let mut manual_overlay = + ManuallyDrop::new(self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + )); let (base_cursor, overlay_statuses) = if manual_overlay.is_some() { let bounds = self.bounds; @@ -215,9 +224,12 @@ where &layout::Limits::new(Size::ZERO, self.bounds), ); - manual_overlay = ManuallyDrop::new( - self.root.overlay(Layout::new(&self.base), renderer), - ); + manual_overlay = + ManuallyDrop::new(self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + )); if manual_overlay.is_none() { break; @@ -262,7 +274,8 @@ where let mut shell = Shell::new(messages); - let event_status = self.root.widget.on_event( + let event_status = self.root.as_widget_mut().on_event( + &mut self.state, event, Layout::new(&self.base), base_cursor, @@ -377,9 +390,11 @@ where let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = - self.root.overlay(Layout::new(&self.base), renderer) - { + let base_cursor = if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { let overlay_layout = self .overlay .take() @@ -399,7 +414,8 @@ where cursor_position }; - self.root.widget.draw( + self.root.as_widget().draw( + &self.state, renderer, theme, style, @@ -408,7 +424,8 @@ where &viewport, ); - let base_interaction = self.root.widget.mouse_interaction( + let base_interaction = self.root.as_widget().mouse_interaction( + &self.state, Layout::new(&self.base), cursor_position, &viewport, @@ -430,52 +447,79 @@ where overlay .as_ref() .and_then(|layout| { - root.overlay(Layout::new(base), renderer).map(|overlay| { - let overlay_interaction = overlay.mouse_interaction( - Layout::new(layout), - cursor_position, - &viewport, - renderer, - ); - - let overlay_bounds = layout.bounds(); - - renderer.with_layer(overlay_bounds, |renderer| { - overlay.draw( - renderer, - theme, - style, + root.as_widget() + .overlay(&mut self.state, Layout::new(base), renderer) + .map(|overlay| { + let overlay_interaction = overlay.mouse_interaction( Layout::new(layout), cursor_position, + &viewport, + renderer, ); - }); - if overlay_bounds.contains(cursor_position) { - overlay_interaction - } else { - base_interaction - } - }) + let overlay_bounds = layout.bounds(); + + renderer.with_layer(overlay_bounds, |renderer| { + overlay.draw( + renderer, + theme, + style, + Layout::new(layout), + cursor_position, + ); + }); + + if overlay_bounds.contains(cursor_position) { + overlay_interaction + } else { + base_interaction + } + }) }) .unwrap_or(base_interaction) } + /// Applies a [`widget::Operation`] to the [`UserInterface`]. + pub fn operate( + &mut self, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + self.root.as_widget().operate( + &mut self.state, + Layout::new(&self.base), + operation, + ); + + if let Some(layout) = self.overlay.as_ref() { + if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { + overlay.operate(Layout::new(layout), operation); + } + } + } + /// Relayouts and returns a new [`UserInterface`] using the provided /// bounds. pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { - Self::build(self.root, bounds, Cache, renderer) + Self::build(self.root, bounds, Cache { state: self.state }, renderer) } /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the /// process. pub fn into_cache(self) -> Cache { - Cache + Cache { state: self.state } } } /// Reusable data of a specific [`UserInterface`]. -#[derive(Debug, Clone)] -pub struct Cache; +#[derive(Debug)] +pub struct Cache { + state: widget::Tree, +} impl Cache { /// Creates an empty [`Cache`]. @@ -483,7 +527,9 @@ impl Cache { /// You should use this to initialize a [`Cache`] before building your first /// [`UserInterface`]. pub fn new() -> Cache { - Cache + Cache { + state: widget::Tree::empty(), + } } } diff --git a/native/src/widget.rs b/native/src/widget.rs index 9fe96e33..8890b8e7 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -15,7 +15,9 @@ pub mod button; pub mod checkbox; pub mod column; pub mod container; +pub mod helpers; pub mod image; +pub mod operation; pub mod pane_grid; pub mod pick_list; pub mod progress_bar; @@ -30,6 +32,10 @@ pub mod text; pub mod text_input; pub mod toggler; pub mod tooltip; +pub mod tree; + +mod action; +mod id; #[doc(no_inline)] pub use button::Button; @@ -40,6 +46,8 @@ pub use column::Column; #[doc(no_inline)] pub use container::Container; #[doc(no_inline)] +pub use helpers::*; +#[doc(no_inline)] pub use image::Image; #[doc(no_inline)] pub use pane_grid::PaneGrid; @@ -69,6 +77,12 @@ pub use text_input::TextInput; pub use toggler::Toggler; #[doc(no_inline)] pub use tooltip::Tooltip; +#[doc(no_inline)] +pub use tree::Tree; + +pub use action::Action; +pub use id::Id; +pub use operation::Operation; use crate::event::{self, Event}; use crate::layout; @@ -109,12 +123,10 @@ where /// Returns the height of the [`Widget`]. fn height(&self) -> Length; - /// Returns the [`Node`] of the [`Widget`]. + /// Returns the [`layout::Node`] of the [`Widget`]. /// - /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. - /// - /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -124,6 +136,7 @@ where /// Draws the [`Widget`] using the associated `Renderer`. fn draw( &self, + state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -132,20 +145,43 @@ where viewport: &Rectangle, ); - /// Processes a runtime [`Event`]. + /// Returns the [`Tag`] of the [`Widget`]. /// - /// It receives: - /// * an [`Event`] describing user interaction - /// * the computed [`Layout`] of the [`Widget`] - /// * the current cursor position - /// * a mutable `Message` list, allowing the [`Widget`] to produce - /// new messages based on user interaction. - /// * the `Renderer` - /// * a [`Clipboard`], if available + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec<Tree> { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _state: &mut Tree, + _layout: Layout<'_>, + _operation: &mut dyn Operation<Message>, + ) { + } + + /// Processes a runtime [`Event`]. /// /// By default, it does nothing. fn on_event( &mut self, + _state: &mut Tree, _event: Event, _layout: Layout<'_>, _cursor_position: Point, @@ -161,6 +197,7 @@ where /// By default, it returns [`mouse::Interaction::Idle`]. fn mouse_interaction( &self, + _state: &Tree, _layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, @@ -170,11 +207,12 @@ where } /// Returns the overlay of the [`Widget`], if there is any. - fn overlay( - &mut self, + fn overlay<'a>( + &'a self, + _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { + ) -> Option<overlay::Element<'a, Message, Renderer>> { None } } diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs new file mode 100644 index 00000000..766e902b --- /dev/null +++ b/native/src/widget/action.rs @@ -0,0 +1,88 @@ +use crate::widget::operation::{self, Operation}; +use crate::widget::Id; + +use iced_futures::MaybeSend; + +/// An operation to be performed on the widget tree. +#[allow(missing_debug_implementations)] +pub struct Action<T>(Box<dyn Operation<T>>); + +impl<T> Action<T> { + /// Creates a new [`Action`] with the given [`Operation`]. + pub fn new(operation: impl Operation<T> + 'static) -> Self { + Self(Box::new(operation)) + } + + /// Maps the output of an [`Action`] using the given function. + pub fn map<A>( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action<A> + where + T: 'static, + A: 'static, + { + Action(Box::new(Map { + operation: self.0, + f: Box::new(f), + })) + } + + /// Consumes the [`Action`] and returns the internal [`Operation`]. + pub fn into_operation(self) -> Box<dyn Operation<T>> { + self.0 + } +} + +#[allow(missing_debug_implementations)] +struct Map<A, B> { + operation: Box<dyn Operation<A>>, + f: Box<dyn Fn(A) -> B>, +} + +impl<A, B> Operation<B> for Map<A, B> +where + A: 'static, + B: 'static, +{ + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + struct MapRef<'a, A, B> { + operation: &'a mut dyn Operation<A>, + f: &'a dyn Fn(A) -> B, + } + + impl<'a, A, B> Operation<B> for MapRef<'a, A, B> { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + let Self { operation, f } = self; + + operation.container(id, &mut |operation| { + operate_on_children(&mut MapRef { operation, f }); + }); + } + } + + let Self { operation, f } = self; + + MapRef { + operation: operation.as_mut(), + f, + } + .container(id, operate_on_children); + } + + fn focusable( + &mut self, + state: &mut dyn operation::Focusable, + id: Option<&Id>, + ) { + self.operation.focusable(state, id); + } +} diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index a33ee7f7..6c0b8f6e 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -7,6 +7,8 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget::tree::{self, Tree}; +use crate::widget::Operation; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, @@ -17,8 +19,6 @@ pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. /// /// ``` -/// # use iced_native::widget::{button, Text}; -/// # /// # type Button<'a, Message> = /// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # @@ -27,17 +27,13 @@ pub use iced_style::button::{Appearance, StyleSheet}; /// ButtonPressed, /// } /// -/// let mut state = button::State::new(); -/// let button = Button::new(&mut state, Text::new("Press me!")) -/// .on_press(Message::ButtonPressed); +/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); /// ``` /// /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will /// be disabled: /// /// ``` -/// # use iced_native::widget::{button, Text}; -/// # /// # type Button<'a, Message> = /// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # @@ -46,12 +42,12 @@ pub use iced_style::button::{Appearance, StyleSheet}; /// ButtonPressed, /// } /// -/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { -/// Button::new(state, Text::new("I'm disabled!")) +/// fn disabled_button<'a>() -> Button<'a, Message> { +/// Button::new("I'm disabled!") /// } /// -/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { -/// disabled_button(state).on_press(Message::ButtonPressed) +/// fn enabled_button<'a>() -> Button<'a, Message> { +/// disabled_button().on_press(Message::ButtonPressed) /// } /// ``` #[allow(missing_debug_implementations)] @@ -60,7 +56,6 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, content: Element<'a, Message, Renderer>, on_press: Option<Message>, width: Length, @@ -71,24 +66,18 @@ where impl<'a, Message, Renderer> Button<'a, Message, Renderer> where - Message: Clone, Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - /// Creates a new [`Button`] with some local [`State`] and the given - /// content. - pub fn new<E>(state: &'a mut State, content: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { + /// Creates a new [`Button`] with the given content. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { Button { - state, content: content.into(), on_press: None, width: Length::Shrink, height: Length::Shrink, padding: Padding::new(5), - style: Default::default(), + style: <Renderer::Theme as StyleSheet>::Style::default(), } } @@ -111,13 +100,14 @@ where } /// Sets the message that will be produced when the [`Button`] is pressed. - /// If on_press isn't set, button will be disabled. + /// + /// Unless `on_press` is called, the [`Button`] will be disabled. pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self } - /// Sets the style of this [`Button`]. + /// Sets the style variant of this [`Button`]. pub fn style( mut self, style: <Renderer::Theme as StyleSheet>::Style, @@ -127,6 +117,174 @@ where } } +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Button<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || tree.state.downcast_mut::<State>(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let styling = draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + theme, + self.style, + || tree.state.downcast_ref::<State>(), + ); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: styling.text_color, + }, + content_layout, + cursor_position, + &bounds, + ); + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position, self.on_press.is_some()) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: crate::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(button: Button<'a, Message, Renderer>) -> Self { + Self::new(button) + } +} + /// The local state of a [`Button`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { @@ -292,131 +450,3 @@ pub fn mouse_interaction( mouse::Interaction::default() } } - -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Button<'a, Message, Renderer> -where - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| self.content.layout(renderer, limits), - ) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - if let event::Status::Captured = self.content.on_event( - event.clone(), - layout.children().next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ) { - return event::Status::Captured; - } - - update( - event, - layout, - cursor_position, - shell, - &self.on_press, - || &mut self.state, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_press.is_some()) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - - let styling = draw( - renderer, - bounds, - cursor_position, - self.on_press.is_some(), - theme, - self.style, - || self.state, - ); - - self.content.draw( - renderer, - theme, - &renderer::Style { - text_color: styling.text_color, - }, - content_layout, - cursor_position, - &bounds, - ); - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.content - .overlay(layout.children().next().unwrap(), renderer) - } -} - -impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - button: Button<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(button) - } -} diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index a49d2fa2..dc3c0bd0 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -6,7 +6,7 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::touch; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget, @@ -168,6 +168,7 @@ where fn on_event( &mut self, + _tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -194,6 +195,7 @@ where fn mouse_interaction( &self, + _tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -208,6 +210,7 @@ where fn draw( &self, + _tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 4eee7d3c..a8b0f183 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -4,6 +4,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -19,7 +20,6 @@ pub struct Column<'a, Message, Renderer> { width: Length, height: Length, max_width: u32, - max_height: u32, align_items: Alignment, children: Vec<Element<'a, Message, Renderer>>, } @@ -40,7 +40,6 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, - max_height: u32::MAX, align_items: Alignment::Start, children, } @@ -80,12 +79,6 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the maximum height of the [`Column`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the horizontal alignment of the contents of the [`Column`] . pub fn align_items(mut self, align: Alignment) -> Self { self.align_items = align; @@ -93,10 +86,10 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { } /// Adds an element to the [`Column`]. - pub fn push<E>(mut self, child: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { self.children.push(child.into()); self } @@ -113,6 +106,14 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> where Renderer: crate::Renderer, { + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + fn width(&self) -> Length { self.width } @@ -128,7 +129,6 @@ where ) -> layout::Node { let limits = limits .max_width(self.max_width) - .max_height(self.max_height) .width(self.width) .height(self.height); @@ -143,8 +143,26 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -154,9 +172,11 @@ where ) -> event::Status { self.children .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.on_event( + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, event.clone(), layout, cursor_position, @@ -170,6 +190,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -177,9 +198,11 @@ where ) -> mouse::Interaction { self.children .iter() + .zip(&tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.mouse_interaction( + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor_position, viewport, @@ -192,6 +215,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -199,8 +223,14 @@ where cursor_position: Point, viewport: &Rectangle, ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw( + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, @@ -211,30 +241,23 @@ where } } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.children - .iter_mut() - .zip(layout.children()) - .filter_map(|(child, layout)| { - child.widget.overlay(layout, renderer) - }) - .next() + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&self.children, tree, layout, renderer) } } impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: crate::Renderer + 'a, { - fn from( - column: Column<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) + fn from(column: Column<'a, Message, Renderer>) -> Self { + Self::new(column) } } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 3d68a595..2afad3f2 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,6 +5,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -121,46 +122,20 @@ where } } -/// Computes the layout of a [`Container`]. -pub fn layout<Renderer>( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - max_width: u32, - max_height: u32, - padding: Padding, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits - .loose() - .max_width(max_width) - .max_height(max_height) - .width(width) - .height(height) - .pad(padding); - - let mut content = layout_content(renderer, &limits.loose()); - let size = limits.resolve(content.size()); - - content.move_to(Point::new(padding.left.into(), padding.top.into())); - content.align( - Alignment::from(horizontal_alignment), - Alignment::from(vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(padding), vec![content]) -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for Container<'a, Message, Renderer> where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn width(&self) -> Length { self.width } @@ -184,12 +159,30 @@ where self.padding, self.horizontal_alignment, self.vertical_alignment, - |renderer, limits| self.content.layout(renderer, limits), + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -197,7 +190,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.content.widget.on_event( + self.content.as_widget_mut().on_event( + &mut tree.children[0], event, layout.children().next().unwrap(), cursor_position, @@ -209,12 +203,14 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content.widget.mouse_interaction( + self.content.as_widget().mouse_interaction( + &tree.children[0], layout.children().next().unwrap(), cursor_position, viewport, @@ -224,6 +220,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, renderer_style: &renderer::Style, @@ -235,7 +232,8 @@ where draw_background(renderer, &style, layout.bounds()); - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, theme, &renderer::Style { @@ -249,16 +247,68 @@ where ); } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.content - .overlay(layout.children().next().unwrap(), renderer) + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + column: Container<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) } } +/// Computes the layout of a [`Container`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_width: u32, + max_height: u32, + padding: Padding, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_width(max_width) + .max_height(max_height) + .width(width) + .height(height) + .pad(padding); + + let mut content = layout_content(renderer, &limits.loose()); + let size = limits.resolve(content.size()); + + content.move_to(Point::new(padding.left.into(), padding.top.into())); + content.align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + ); + + layout::Node::with_children(size.pad(padding), vec![content]) +} + /// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. pub fn draw_background<Renderer>( renderer: &mut Renderer, @@ -281,17 +331,3 @@ pub fn draw_background<Renderer>( ); } } - -impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - column: Container<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) - } -} diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs new file mode 100644 index 00000000..a62448e9 --- /dev/null +++ b/native/src/widget/helpers.rs @@ -0,0 +1,283 @@ +//! Helper functions to create pure widgets. +use crate::widget; +use crate::{Element, Length}; + +use std::borrow::Cow; +use std::ops::RangeInclusive; + +/// Creates a [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +#[macro_export] +macro_rules! column { + () => ( + $crate::widget::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+]) + ); +} + +/// Creates a [Row`] with the given children. +/// +/// [`Row`]: widget::Row +#[macro_export] +macro_rules! row { + () => ( + $crate::widget::Row::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+]) + ); +} + +/// Creates a new [`Container`] with the provided content. +/// +/// [`Container`]: widget::Container +pub fn container<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Container<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::container::StyleSheet, +{ + widget::Container::new(content) +} + +/// Creates a new [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +pub fn column<Message, Renderer>( + children: Vec<Element<'_, Message, Renderer>>, +) -> widget::Column<'_, Message, Renderer> { + widget::Column::with_children(children) +} + +/// Creates a new [`Row`] with the given children. +/// +/// [`Row`]: widget::Row +pub fn row<Message, Renderer>( + children: Vec<Element<'_, Message, Renderer>>, +) -> widget::Row<'_, Message, Renderer> { + widget::Row::with_children(children) +} + +/// Creates a new [`Scrollable`] with the provided content. +/// +/// [`Scrollable`]: widget::Scrollable +pub fn scrollable<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::scrollable::StyleSheet, +{ + widget::Scrollable::new(content) +} + +/// Creates a new [`Button`] with the provided content. +/// +/// [`Button`]: widget::Button +pub fn button<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Button<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::button::StyleSheet, +{ + widget::Button::new(content) +} + +/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`]. +/// +/// [`Tooltip`]: widget::Tooltip +/// [`tooltip::Position`]: widget::tooltip::Position +pub fn tooltip<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl ToString, + position: widget::tooltip::Position, +) -> widget::Tooltip<'a, Message, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet, +{ + widget::Tooltip::new(content, tooltip, position) +} + +/// Creates a new [`Text`] widget with the provided content. +/// +/// [`Text`]: widget::Text +pub fn text<Renderer>(text: impl ToString) -> widget::Text<Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::text::StyleSheet, +{ + widget::Text::new(text) +} + +/// Creates a new [`Checkbox`]. +/// +/// [`Checkbox`]: widget::Checkbox +pub fn checkbox<'a, Message, Renderer>( + label: impl Into<String>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Checkbox<'a, Message, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet, +{ + widget::Checkbox::new(is_checked, label, f) +} + +/// Creates a new [`Radio`]. +/// +/// [`Radio`]: widget::Radio +pub fn radio<Message, Renderer, V>( + label: impl Into<String>, + value: V, + selected: Option<V>, + on_click: impl FnOnce(V) -> Message, +) -> widget::Radio<Message, Renderer> +where + Message: Clone, + Renderer: crate::text::Renderer, + Renderer::Theme: widget::radio::StyleSheet, + V: Copy + Eq, +{ + widget::Radio::new(value, label, selected, on_click) +} + +/// Creates a new [`Toggler`]. +/// +/// [`Toggler`]: widget::Toggler +pub fn toggler<'a, Message, Renderer>( + label: impl Into<Option<String>>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Toggler<'a, Message, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::toggler::StyleSheet, +{ + widget::Toggler::new(is_checked, label, f) +} + +/// Creates a new [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn text_input<'a, Message, Renderer>( + placeholder: &str, + value: &str, + on_change: impl Fn(String) -> Message + 'a, +) -> widget::TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: crate::text::Renderer, + Renderer::Theme: widget::text_input::StyleSheet, +{ + widget::TextInput::new(placeholder, value, on_change) +} + +/// Creates a new [`Slider`]. +/// +/// [`Slider`]: widget::Slider +pub fn slider<'a, T, Message, Renderer>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> widget::Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: widget::slider::StyleSheet, +{ + widget::Slider::new(range, value, on_change) +} + +/// Creates a new [`PickList`]. +/// +/// [`PickList`]: widget::PickList +pub fn pick_list<'a, Message, Renderer, T>( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, +) -> widget::PickList<'a, T, Message, Renderer> +where + T: ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: crate::text::Renderer, + Renderer::Theme: widget::pick_list::StyleSheet, +{ + widget::PickList::new(options, selected, on_selected) +} + +/// Creates a new [`Image`]. +/// +/// [`Image`]: widget::Image +pub fn image<Handle>(handle: impl Into<Handle>) -> widget::Image<Handle> { + widget::Image::new(handle.into()) +} + +/// Creates a new horizontal [`Space`] with the given [`Length`]. +/// +/// [`Space`]: widget::Space +pub fn horizontal_space(width: Length) -> widget::Space { + widget::Space::with_width(width) +} + +/// Creates a new vertical [`Space`] with the given [`Length`]. +/// +/// [`Space`]: widget::Space +pub fn vertical_space(height: Length) -> widget::Space { + widget::Space::with_height(height) +} + +/// Creates a horizontal [`Rule`] with the given height. +/// +/// [`Rule`]: widget::Rule +pub fn horizontal_rule<Renderer>(height: u16) -> widget::Rule<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::rule::StyleSheet, +{ + widget::Rule::horizontal(height) +} + +/// Creates a vertical [`Rule`] with the given width. +/// +/// [`Rule`]: widget::Rule +pub fn vertical_rule<Renderer>(width: u16) -> widget::Rule<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::rule::StyleSheet, +{ + widget::Rule::vertical(width) +} + +/// Creates a new [`ProgressBar`]. +/// +/// It expects: +/// * an inclusive range of possible values, and +/// * the current value of the [`ProgressBar`]. +/// +/// [`ProgressBar`]: widget::ProgressBar +pub fn progress_bar<Renderer>( + range: RangeInclusive<f32>, + value: f32, +) -> widget::ProgressBar<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::progress_bar::StyleSheet, +{ + widget::ProgressBar::new(range, value) +} + +/// Creates a new [`Svg`] widget from the given [`Handle`]. +/// +/// [`Svg`]: widget::Svg +/// [`Handle`]: widget::svg::Handle +pub fn svg(handle: impl Into<widget::svg::Handle>) -> widget::Svg { + widget::Svg::new(handle) +} diff --git a/native/src/widget/id.rs b/native/src/widget/id.rs new file mode 100644 index 00000000..4b8fedf1 --- /dev/null +++ b/native/src/widget/id.rs @@ -0,0 +1,43 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self { + Self(Internal::Custom(id.into())) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 72075bd1..91d68e34 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -5,12 +5,18 @@ pub use viewer::Viewer; use crate::image; use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::hash::Hash; +/// Creates a new [`Viewer`] with the given image `Handle`. +pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { + Viewer::new(handle) +} + /// A frame that displays an image while keeping aspect ratio. /// /// # Example @@ -135,6 +141,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 1aa75aa0..b1fe596c 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -4,6 +4,7 @@ use crate::image; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, @@ -13,8 +14,7 @@ use std::hash::Hash; /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] -pub struct Viewer<'a, Handle> { - state: &'a mut State, +pub struct Viewer<Handle> { padding: u16, width: Length, height: Length, @@ -24,11 +24,10 @@ pub struct Viewer<'a, Handle> { handle: Handle, } -impl<'a, Handle> Viewer<'a, Handle> { +impl<Handle> Viewer<Handle> { /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(state: &'a mut State, handle: Handle) -> Self { + pub fn new(handle: Handle) -> Self { Viewer { - state, padding: 0, width: Length::Shrink, height: Length::Shrink, @@ -81,43 +80,21 @@ impl<'a, Handle> Viewer<'a, Handle> { self.scale_step = scale_step; self } - - /// Returns the bounds of the underlying image, given the bounds of - /// the [`Viewer`]. Scaling will be applied and original aspect ratio - /// will be respected. - fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size - where - Renderer: image::Renderer<Handle = Handle>, - { - let (width, height) = renderer.dimensions(&self.handle); - - 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 = self.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) - } } -impl<'a, Message, Renderer, Handle> Widget<Message, Renderer> - for Viewer<'a, Handle> +impl<Message, Renderer, Handle> Widget<Message, Renderer> for Viewer<Handle> where Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + fn width(&self) -> Length { self.width } @@ -164,6 +141,7 @@ where fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -181,39 +159,43 @@ where match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - let previous_scale = self.state.scale; + let state = tree.state.downcast_mut::<State>(); + let previous_scale = state.scale; if y < 0.0 && previous_scale > self.min_scale || y > 0.0 && previous_scale < self.max_scale { - self.state.scale = (if y > 0.0 { - self.state.scale * (1.0 + self.scale_step) + state.scale = (if y > 0.0 { + state.scale * (1.0 + self.scale_step) } else { - self.state.scale / (1.0 + self.scale_step) + state.scale / (1.0 + self.scale_step) }) .max(self.min_scale) .min(self.max_scale); - let image_size = - self.image_size(renderer, bounds.size()); + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); - let factor = - self.state.scale / previous_scale - 1.0; + let factor = state.scale / previous_scale - 1.0; let cursor_to_center = cursor_position - bounds.center(); let adjustment = cursor_to_center * factor - + self.state.current_offset * factor; + + state.current_offset * factor; - self.state.current_offset = Vector::new( + state.current_offset = Vector::new( if image_size.width > bounds.width { - self.state.current_offset.x + adjustment.x + state.current_offset.x + adjustment.x } else { 0.0 }, if image_size.height > bounds.height { - self.state.current_offset.y + adjustment.y + state.current_offset.y + adjustment.y } else { 0.0 }, @@ -227,21 +209,34 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if is_mouse_over => { - self.state.cursor_grabbed_at = Some(cursor_position); - self.state.starting_offset = self.state.current_offset; + let state = tree.state.downcast_mut::<State>(); + + state.cursor_grabbed_at = Some(cursor_position); + state.starting_offset = state.current_offset; event::Status::Captured } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - if self.state.cursor_grabbed_at.is_some() => - { - self.state.cursor_grabbed_at = None; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree.state.downcast_mut::<State>(); - event::Status::Captured + if state.cursor_grabbed_at.is_some() { + state.cursor_grabbed_at = None; + + event::Status::Captured + } else { + event::Status::Ignored + } } Event::Mouse(mouse::Event::CursorMoved { position }) => { - if let Some(origin) = self.state.cursor_grabbed_at { - let image_size = self.image_size(renderer, bounds.size()); + let state = tree.state.downcast_mut::<State>(); + + if let Some(origin) = state.cursor_grabbed_at { + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); let hidden_width = (image_size.width - bounds.width / 2.0) .max(0.0) @@ -255,7 +250,7 @@ where let delta = position - origin; let x = if bounds.width < image_size.width { - (self.state.starting_offset.x - delta.x) + (state.starting_offset.x - delta.x) .min(hidden_width) .max(-hidden_width) } else { @@ -263,14 +258,14 @@ where }; let y = if bounds.height < image_size.height { - (self.state.starting_offset.y - delta.y) + (state.starting_offset.y - delta.y) .min(hidden_height) .max(-hidden_height) } else { 0.0 }; - self.state.current_offset = Vector::new(x, y); + state.current_offset = Vector::new(x, y); event::Status::Captured } else { @@ -283,15 +278,17 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); - if self.state.is_cursor_grabbed() { + if state.is_cursor_grabbed() { mouse::Interaction::Grabbing } else if is_mouse_over { mouse::Interaction::Grab @@ -302,6 +299,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, @@ -309,9 +307,11 @@ where _cursor_position: Point, _viewport: &Rectangle, ) { + let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let image_size = self.image_size(renderer, bounds.size()); + let image_size = + image_size(renderer, &self.handle, state, bounds.size()); let translation = { let image_top_left = Vector::new( @@ -319,7 +319,7 @@ where bounds.height / 2.0 - image_size.height / 2.0, ); - image_top_left - self.state.offset(bounds, image_size) + image_top_left - state.offset(bounds, image_size) }; renderer.with_layer(bounds, |renderer| { @@ -385,14 +385,47 @@ impl State { } } -impl<'a, Message, Renderer, Handle> From<Viewer<'a, Handle>> +impl<'a, Message, Renderer, Handle> From<Viewer<Handle>> for Element<'a, Message, Renderer> where Renderer: 'a + image::Renderer<Handle = Handle>, Message: 'a, Handle: Clone + Hash + 'a, { - fn from(viewer: Viewer<'a, Handle>) -> Element<'a, Message, Renderer> { + fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Renderer> { Element::new(viewer) } } + +/// 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<Renderer>( + renderer: &Renderer, + handle: &<Renderer as image::Renderer>::Handle, + state: &State, + bounds: Size, +) -> Size +where + Renderer: image::Renderer, +{ + let (width, height) = renderer.dimensions(handle); + + 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) +} diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs new file mode 100644 index 00000000..ef636aa2 --- /dev/null +++ b/native/src/widget/operation.rs @@ -0,0 +1,60 @@ +//! Query or update internal widget state. +pub mod focusable; +pub mod scrollable; + +pub use focusable::Focusable; +pub use scrollable::Scrollable; + +use crate::widget::Id; + +use std::fmt; + +/// 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> { + /// Operates on a widget that contains other widgets. + /// + /// The `operate_on_children` function can be called to return control to + /// the widget tree and keep traversing it. + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ); + + /// Operates on a widget that can be focused. + fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} + + /// Operates on a widget that can be scrolled. + fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + + /// Finishes the [`Operation`] and returns its [`Outcome`]. + fn finish(&self) -> Outcome<T> { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome<T> { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box<dyn Operation<T>>), +} + +impl<T> fmt::Debug for Outcome<T> +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({:?})", output), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs new file mode 100644 index 00000000..f17bf178 --- /dev/null +++ b/native/src/widget/operation/focusable.rs @@ -0,0 +1,169 @@ +//! Operate on widgets that can be focused. +use crate::widget::operation::{Operation, Outcome}; +use crate::widget::Id; + +/// The internal state of a widget that can be focused. +pub trait Focusable { + /// Returns whether the widget is focused or not. + fn is_focused(&self) -> bool; + + /// Focuses the widget. + fn focus(&mut self); + + /// Unfocuses the widget. + fn unfocus(&mut self); +} + +/// A summary of the focusable widgets present on a widget tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Count { + /// The index of the current focused widget, if any. + focused: Option<usize>, + + /// The total amount of focusable widgets. + total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus<T>(target: Id) -> impl Operation<T> { + struct Focus { + target: Id, + } + + impl<T> Operation<T> for Focus { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.focus(); + } + _ => { + state.unfocus(); + } + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + Focus { target } +} + +/// 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> { + count: Count, + next: fn(Count) -> O, + } + + impl<T, O> Operation<T> for CountFocusable<O> + where + O: Operation<T> + 'static, + { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if state.is_focused() { + self.count.focused = Some(self.count.total); + } + + self.count.total += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome<T> { + Outcome::Chain(Box::new((self.next)(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> { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl<T> Operation<T> for FocusPrevious { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if self.count.total == 0 { + return; + } + + match self.count.focused { + None if self.current == self.count.total - 1 => state.focus(), + Some(0) if self.current == 0 => state.unfocus(), + Some(0) => {} + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused - 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + 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> { + struct FocusNext { + count: Count, + current: usize, + } + + impl<T> Operation<T> for FocusNext { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + match self.count.focused { + None if self.current == 0 => state.focus(), + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused + 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + count(|count| FocusNext { count, current: 0 }) +} diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs new file mode 100644 index 00000000..2210137d --- /dev/null +++ b/native/src/widget/operation/scrollable.rs @@ -0,0 +1,35 @@ +//! Operate on widgets that can be scrolled. +use crate::widget::{Id, Operation}; + +/// The internal state of a widget that can be scrolled. +pub trait Scrollable { + /// Snaps the scroll of the widget to the given `percentage`. + fn snap_to(&mut self, percentage: f32); +} + +/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to +/// the provided `percentage`. +pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> { + struct SnapTo { + target: Id, + percentage: f32, + } + + impl<T> Operation<T> for SnapTo { + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.percentage); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + SnapTo { target, percentage } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 70ca772c..d84fb7a0 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -30,6 +30,8 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; +pub use iced_style::pane_grid::{Line, StyleSheet}; + use crate::event::{self, Event}; use crate::layout; use crate::mouse; @@ -37,13 +39,12 @@ use crate::overlay; use crate::renderer; use crate::touch; use crate::widget::container; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; -pub use iced_style::pane_grid::{Line, StyleSheet}; - /// A collection of panes distributed using either vertical or horizontal splits /// to completely fill the space available. /// @@ -66,7 +67,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// ## Example /// /// ``` -/// # use iced_native::widget::{pane_grid, Text}; +/// # use iced_native::widget::{pane_grid, text}; /// # /// # type PaneGrid<'a, Message> = /// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; @@ -84,10 +85,10 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state| { +/// PaneGrid::new(&state, |pane, state| { /// pane_grid::Content::new(match state { -/// PaneState::SomePane => Text::new("This is some pane"), -/// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), +/// PaneState::SomePane => text("This is some pane"), +/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), /// }) /// }) /// .on_drag(Message::PaneDragged) @@ -99,8 +100,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { - state: &'a mut state::Internal, - action: &'a mut state::Action, + state: &'a state::Internal, elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, @@ -121,21 +121,20 @@ where /// The view function will be called to display each [`Pane`] present in the /// [`State`]. pub fn new<T>( - state: &'a mut State<T>, - view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, + state: &'a State<T>, + view: impl Fn(Pane, &'a T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { state .panes - .iter_mut() + .iter() .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; Self { - state: &mut state.internal, - action: &mut state.action, elements, + state: &state.internal, width: Length::Fill, height: Length::Fill, spacing: 0, @@ -211,6 +210,220 @@ where } } +impl<'a, Message, Renderer> Widget<Message, Renderer> + for PaneGrid<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<state::Action>() + } + + fn state(&self) -> tree::State { + tree::State::new(state::Action::Idle) + } + + fn children(&self) -> Vec<Tree> { + self.elements + .iter() + .map(|(_, content)| content.state()) + .collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children_custom( + &self.elements, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.state, + self.width, + self.height, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + |element, renderer, limits| element.layout(renderer, limits), + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let action = tree.state.downcast_mut::<state::Action>(); + + let event_status = update( + action, + self.state, + &event, + layout, + cursor_position, + shell, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + &self.on_click, + &self.on_drag, + &self.on_resize, + ); + + let picked_pane = action.picked_pane().map(|(pane, _)| pane); + + self.elements + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|(((pane, content), tree), layout)| { + let is_picked = picked_pane == Some(*pane); + + content.on_event( + tree, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.elements + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + renderer, + theme, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style, + self.elements + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (*pane, (content, tree))), + |(content, tree), + renderer, + style, + layout, + cursor_position, + rectangle| { + content.draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + rectangle, + ); + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|(((_, pane), tree), layout)| { + pane.overlay(tree, layout, renderer) + }) + .next() + } +} + +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} + /// Calculates the [`Layout`] of a [`PaneGrid`]. pub fn layout<Renderer, T>( renderer: &Renderer, @@ -656,175 +869,6 @@ pub struct ResizeEvent { pub ratio: f32, } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.state, - self.width, - self.height, - self.spacing, - self.elements.iter().map(|(pane, content)| (*pane, content)), - |element, renderer, limits| element.layout(renderer, limits), - ) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let event_status = update( - self.action, - self.state, - &event, - layout, - cursor_position, - shell, - self.spacing, - self.elements.iter().map(|(pane, content)| (*pane, content)), - &self.on_click, - &self.on_drag, - &self.on_resize, - ); - - let picked_pane = self.action.picked_pane().map(|(pane, _)| pane); - - self.elements - .iter_mut() - .zip(layout.children()) - .map(|((pane, content), layout)| { - let is_picked = picked_pane == Some(*pane); - - content.on_event( - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - is_picked, - ) - }) - .fold(event_status, event::Status::merge) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - self.action, - self.state, - layout, - cursor_position, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - ) - .unwrap_or_else(|| { - self.elements - .iter() - .zip(layout.children()) - .map(|((_pane, content), layout)| { - content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - }) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - draw( - self.action, - self.state, - layout, - cursor_position, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - self.style, - self.elements.iter().map(|(pane, content)| (*pane, content)), - |pane, renderer, style, layout, cursor_position, rectangle| { - pane.draw( - renderer, - theme, - style, - layout, - cursor_position, - rectangle, - ); - }, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.elements - .iter_mut() - .zip(layout.children()) - .filter_map(|((_, pane), layout)| pane.overlay(layout, renderer)) - .next() - } -} - -impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn from( - pane_grid: PaneGrid<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(pane_grid) - } -} - /* * Helpers */ diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 4c9e65c9..98ce2c4b 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -5,6 +5,7 @@ use crate::overlay; use crate::renderer; use crate::widget::container; use crate::widget::pane_grid::{Draggable, TitleBar}; +use crate::widget::Tree; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The content of a [`Pane`]. @@ -59,11 +60,37 @@ where Renderer: crate::Renderer, Renderer::Theme: container::StyleSheet, { + pub(super) fn state(&self) -> Tree { + let children = if let Some(title_bar) = self.title_bar.as_ref() { + vec![Tree::new(&self.body), title_bar.state()] + } else { + vec![Tree::new(&self.body), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub(super) fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(title_bar) = self.title_bar.as_ref() { + title_bar.diff(&mut tree.children[1]); + } + + tree.children[0].diff(&self.body); + } else { + *tree = self.state(); + } + } + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::Renderer + /// [`Renderer`]: iced_native::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -89,6 +116,7 @@ where let show_controls = bounds.contains(cursor_position); title_bar.draw( + &tree.children[1], renderer, theme, style, @@ -98,7 +126,8 @@ where show_controls, ); - self.body.draw( + self.body.as_widget().draw( + &tree.children[0], renderer, theme, style, @@ -107,7 +136,8 @@ where viewport, ); } else { - self.body.draw( + self.body.as_widget().draw( + &tree.children[0], renderer, theme, style, @@ -131,7 +161,7 @@ where let title_bar_size = title_bar_layout.size(); - let mut body_layout = self.body.layout( + let mut body_layout = self.body.as_widget().layout( renderer, &layout::Limits::new( Size::ZERO, @@ -149,12 +179,13 @@ where vec![title_bar_layout, body_layout], ) } else { - self.body.layout(renderer, limits) + self.body.as_widget().layout(renderer, limits) } } pub(crate) fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -169,6 +200,7 @@ where let mut children = layout.children(); event_status = title_bar.on_event( + &mut tree.children[1], event.clone(), children.next().unwrap(), cursor_position, @@ -185,7 +217,8 @@ where let body_status = if is_picked { event::Status::Ignored } else { - self.body.on_event( + self.body.as_widget_mut().on_event( + &mut tree.children[0], event, body_layout, cursor_position, @@ -200,6 +233,7 @@ where pub(crate) fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -218,6 +252,7 @@ where } let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], title_bar_layout, cursor_position, viewport, @@ -230,25 +265,46 @@ where }; self.body - .mouse_interaction(body_layout, cursor_position, viewport, renderer) + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor_position, + viewport, + renderer, + ) .max(title_bar_interaction) } - pub(crate) fn overlay( - &mut self, + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - if let Some(title_bar) = self.title_bar.as_mut() { + ) -> Option<overlay::Element<'b, Message, Renderer>> { + if let Some(title_bar) = self.title_bar.as_ref() { let mut children = layout.children(); let title_bar_layout = children.next()?; - match title_bar.overlay(title_bar_layout, renderer) { + let mut states = tree.children.iter_mut(); + let body_state = states.next().unwrap(); + let title_bar_state = states.next().unwrap(); + + match title_bar.overlay(title_bar_state, title_bar_layout, renderer) + { Some(overlay) => Some(overlay), - None => self.body.overlay(children.next()?, renderer), + None => self.body.as_widget().overlay( + body_state, + children.next()?, + renderer, + ), } } else { - self.body.overlay(layout, renderer) + self.body.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) } } } diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index 4e90f645..cdca6267 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -31,8 +31,6 @@ pub struct State<T> { /// /// [`PaneGrid`]: crate::widget::PaneGrid pub internal: Internal, - - pub(super) action: Action, } impl<T> State<T> { @@ -54,11 +52,7 @@ impl<T> State<T> { let internal = Internal::from_configuration(&mut panes, config.into(), 0); - State { - panes, - internal, - action: Action::Idle, - } + State { panes, internal } } /// Returns the total amount of panes in the [`State`]. diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 14c3ab4e..f9050e3e 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -4,6 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; +use crate::widget::Tree; use crate::{ Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, }; @@ -86,11 +87,37 @@ where Renderer: crate::Renderer, Renderer::Theme: container::StyleSheet, { + 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()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub(super) fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(controls) = self.controls.as_ref() { + tree.children[1].diff(controls); + } + + tree.children[0].diff(&self.content); + } else { + *tree = self.state(); + } + } + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::Renderer + /// [`Renderer`]: iced_native::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, inherited_style: &renderer::Style, @@ -118,14 +145,15 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } if show_controls || self.always_show_controls { - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - controls.draw( + controls.as_widget().draw( + &tree.children[1], renderer, theme, &inherited_style, @@ -137,7 +165,8 @@ where } if show_title { - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, theme, &inherited_style, @@ -186,11 +215,14 @@ where let title_layout = self .content + .as_widget() .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let title_size = title_layout.size(); let mut node = if let Some(controls) = &self.controls { let mut controls_layout = controls + .as_widget() .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); let controls_size = controls_layout.size(); @@ -221,6 +253,7 @@ where pub(crate) fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -243,7 +276,8 @@ where show_title = false; } - controls.on_event( + controls.as_widget_mut().on_event( + &mut tree.children[1], event.clone(), controls_layout, cursor_position, @@ -256,7 +290,8 @@ where }; let title_status = if show_title { - self.content.on_event( + self.content.as_widget_mut().on_event( + &mut tree.children[0], event, title_layout, cursor_position, @@ -273,6 +308,7 @@ where pub(crate) fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -284,7 +320,8 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - let title_interaction = self.content.mouse_interaction( + let title_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], title_layout, cursor_position, viewport, @@ -293,7 +330,8 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let controls_interaction = controls.mouse_interaction( + let controls_interaction = controls.as_widget().mouse_interaction( + &tree.children[1], controls_layout, cursor_position, viewport, @@ -312,11 +350,12 @@ where } } - pub(crate) fn overlay( - &mut self, + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { + ) -> Option<overlay::Element<'b, Message, Renderer>> { let mut children = layout.children(); let padded = children.next()?; @@ -327,12 +366,23 @@ where content, controls, .. } = self; - content.overlay(title_layout, renderer).or_else(move || { - controls.as_mut().and_then(|controls| { - let controls_layout = children.next()?; - - controls.overlay(controls_layout, renderer) + let mut states = tree.children.iter_mut(); + let title_state = states.next().unwrap(); + let controls_state = states.next().unwrap(); + + content + .as_widget() + .overlay(title_state, title_layout, renderer) + .or_else(move || { + controls.as_ref().and_then(|controls| { + let controls_layout = children.next()?; + + controls.as_widget().overlay( + controls_state, + controls_layout, + renderer, + ) + }) }) - }) } } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 61b0eb96..1232878b 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -9,8 +9,7 @@ use crate::overlay::menu::{self, Menu}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; -use crate::widget::container; -use crate::widget::scrollable; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Widget, @@ -27,8 +26,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State<T>, - on_selected: Box<dyn Fn(T) -> Message>, + on_selected: Box<dyn Fn(T) -> Message + 'a>, options: Cow<'a, [T]>, placeholder: Option<String>, selected: Option<T>, @@ -39,35 +37,6 @@ where style: <Renderer::Theme as StyleSheet>::Style, } -/// The local state of a [`PickList`]. -#[derive(Debug, Clone)] -pub struct State<T> { - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option<usize>, - last_selection: Option<T>, -} - -impl<T> State<T> { - /// Creates a new [`State`] for a [`PickList`]. - pub fn new() -> Self { - Self { - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: bool::default(), - hovered_option: Option::default(), - last_selection: Option::default(), - } - } -} - -impl<T> Default for State<T> { - fn default() -> Self { - Self::new() - } -} - impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> where T: ToString + Eq, @@ -78,17 +47,14 @@ where /// The default padding of a [`PickList`]. pub const DEFAULT_PADDING: Padding = Padding::new(5); - /// Creates a new [`PickList`] with the given [`State`], a list of options, - /// the current selected value, and the message to produce when an option is - /// selected. + /// Creates a new [`PickList`] with the given list of options, the current + /// selected value, and the message to produce when an option is selected. pub fn new( - state: &'a mut State<T>, options: impl Into<Cow<'a, [T]>>, selected: Option<T>, - on_selected: impl Fn(T) -> Message + 'static, + on_selected: impl Fn(T) -> Message + 'a, ) -> Self { Self { - state, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -141,6 +107,168 @@ where } } +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<T>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::<T>::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_deref(), + &self.options, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::<State<T>>(), + ) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_deref(), + self.selected.as_ref(), + self.style, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_mut::<State<T>>(); + + overlay( + layout, + state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style, + ) + } +} + +impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { + Self::new(pick_list) + } +} + +/// The local state of a [`PickList`]. +#[derive(Debug)] +pub struct State<T> { + menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, + is_open: bool, + hovered_option: Option<usize>, + last_selection: Option<T>, +} + +impl<T> State<T> { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { + Self { + menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), + is_open: bool::default(), + hovered_option: Option::default(), + last_selection: Option::default(), + } + } +} + +impl<T> Default for State<T> { + fn default() -> Self { + Self::new() + } +} + /// Computes the layout of a [`PickList`]. pub fn layout<Renderer, T>( renderer: &Renderer, @@ -429,127 +557,3 @@ pub fn draw<T, Renderer>( }); } } - -impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> - for PickList<'a, T, Message, Renderer> -where - T: Clone + ToString + Eq, - [T]: ToOwned<Owned = Vec<T>>, - Message: 'static, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.text_size, - &self.font, - self.placeholder.as_deref(), - &self.options, - ) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - event, - layout, - cursor_position, - shell, - self.on_selected.as_ref(), - self.selected.as_ref(), - &self.options, - || &mut self.state, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - theme, - layout, - cursor_position, - self.padding, - self.text_size, - &self.font, - self.placeholder.as_deref(), - self.selected.as_ref(), - self.style, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - overlay( - layout, - self.state, - self.padding, - self.text_size, - self.font.clone(), - &self.options, - self.style, - ) - } -} - -impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>> - for Element<'a, Message, Renderer> -where - T: Clone + ToString + Eq, - [T]: ToOwned<Owned = Vec<T>>, - Message: 'static, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + container::StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet, - <Renderer::Theme as StyleSheet>::Style: - Into<<Renderer::Theme as menu::StyleSheet>::Style>, -{ - fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { - Element::new(pick_list) - } -} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 50bdcda6..8a945433 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -1,6 +1,7 @@ //! Provide progress feedback to your users. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; use std::ops::RangeInclusive; @@ -105,6 +106,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 647e3c5a..c9152d05 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -6,7 +6,7 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::touch; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Widget, @@ -176,6 +176,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -200,6 +201,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -214,6 +216,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 9d8cc715..eda7c2d3 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,16 +1,15 @@ //! Distribute content horizontally. use crate::event::{self, Event}; -use crate::layout; +use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Length, Padding, Point, Rectangle, Shell, + Widget, }; -use std::u32; - /// A container that distributes its contents horizontally. #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { @@ -18,8 +17,6 @@ pub struct Row<'a, Message, Renderer> { padding: Padding, width: Length, height: Length, - max_width: u32, - max_height: u32, align_items: Alignment, children: Vec<Element<'a, Message, Renderer>>, } @@ -39,8 +36,6 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, - max_width: u32::MAX, - max_height: u32::MAX, align_items: Alignment::Start, children, } @@ -74,18 +69,6 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the maximum width of the [`Row`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - - /// Sets the maximum height of the [`Row`]. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the vertical alignment of the contents of the [`Row`] . pub fn align_items(mut self, align: Alignment) -> Self { self.align_items = align; @@ -93,10 +76,10 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { } /// Adds an [`Element`] to the [`Row`]. - pub fn push<E>(mut self, child: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { self.children.push(child.into()); self } @@ -113,6 +96,14 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> where Renderer: crate::Renderer, { + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children) + } + fn width(&self) -> Length { self.width } @@ -126,11 +117,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .max_height(self.max_height) - .width(self.width) - .height(self.height); + let limits = limits.width(self.width).height(self.height); layout::flex::resolve( layout::flex::Axis::Horizontal, @@ -143,8 +130,26 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -154,9 +159,11 @@ where ) -> event::Status { self.children .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.on_event( + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, event.clone(), layout, cursor_position, @@ -170,6 +177,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -177,9 +185,11 @@ where ) -> mouse::Interaction { self.children .iter() + .zip(&tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.mouse_interaction( + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor_position, viewport, @@ -192,6 +202,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -199,8 +210,14 @@ where cursor_position: Point, viewport: &Rectangle, ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw( + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, @@ -211,28 +228,23 @@ where } } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.children - .iter_mut() - .zip(layout.children()) - .filter_map(|(child, layout)| { - child.widget.overlay(layout, renderer) - }) - .next() + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&self.children, tree, layout, renderer) } } impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: crate::Renderer + 'a, { - fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { - Element::new(row) + fn from(row: Row<'a, Message, Renderer>) -> Self { + Self::new(row) } } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index f0fda8a9..56f8c80d 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -1,6 +1,7 @@ //! Display a horizontal or vertical rule for dividing content. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; pub use iced_style::rule::{Appearance, FillMode, StyleSheet}; @@ -78,6 +79,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 77ed2066..4ebb07a0 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -5,10 +5,12 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; -use crate::widget::Column; +use crate::widget; +use crate::widget::operation::{self, Operation}; +use crate::widget::tree::{self, Tree}; use crate::{ - Alignment, Background, Clipboard, Color, Element, Layout, Length, Padding, - Point, Rectangle, Shell, Size, Vector, Widget, + Background, Clipboard, Color, Command, Element, Layout, Length, Point, + Rectangle, Shell, Size, Vector, Widget, }; use std::{f32, u32}; @@ -30,13 +32,12 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, + id: Option<Id>, height: Length, - max_height: u32, scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, - content: Column<'a, Message, Renderer>, + content: Element<'a, Message, Renderer>, on_scroll: Option<Box<dyn Fn(f32) -> Message + 'a>>, style: <Renderer::Theme as StyleSheet>::Style, } @@ -46,40 +47,23 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - /// Creates a new [`Scrollable`] with the given [`State`]. - pub fn new(state: &'a mut State) -> Self { + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { Scrollable { - state, + id: None, height: Length::Shrink, - max_height: u32::MAX, scrollbar_width: 10, scrollbar_margin: 0, scroller_width: 10, - content: Column::new(), + content: content.into(), on_scroll: None, style: Default::default(), } } - /// Sets the vertical spacing _between_ elements. - /// - /// Custom margins per element do not exist in Iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, units: u16) -> Self { - self.content = self.content.spacing(units); - self - } - - /// Sets the [`Padding`] of the [`Scrollable`]. - pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { - self.content = self.content.padding(padding); - self - } - - /// Sets the width of the [`Scrollable`]. - pub fn width(mut self, width: Length) -> Self { - self.content = self.content.width(width); + /// Sets the [`Id`] of the [`Scrollable`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); self } @@ -89,24 +73,6 @@ where self } - /// Sets the maximum width of the [`Scrollable`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.content = self.content.max_width(max_width); - self - } - - /// Sets the maximum height of the [`Scrollable`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - - /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - pub fn align_items(mut self, align_items: Alignment) -> Self { - self.content = self.content.align_items(align_items); - self - } - /// Sets the scrollbar width of the [`Scrollable`] . /// Silently enforces a minimum value of 1. pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { @@ -132,7 +98,7 @@ where /// /// The function takes the new relative offset of the [`Scrollable`] /// (e.g. `0` means top, while `1` means bottom). - pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -145,17 +111,235 @@ where self.style = style.into(); self } +} - /// Adds an element to the [`Scrollable`]. - pub fn push<E>(mut self, child: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { - self.content = self.content.push(child); - self +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::<Message, Renderer>::width(self), + self.height, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + + operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); + + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + tree.state.downcast_mut::<State>(), + event, + layout, + cursor_position, + clipboard, + shell, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::<State>(), + renderer, + theme, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + self.style, + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref::<State>(), + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content + .as_widget() + .overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = tree + .state + .downcast_ref::<State>() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) } } +impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: Scrollable<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`Scrollable`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided `percentage`. +pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> { + Command::widget(operation::scrollable::snap_to(id.0, percentage)) +} + /// Computes the layout of a [`Scrollable`]. pub fn layout<Renderer>( renderer: &Renderer, @@ -625,145 +809,6 @@ fn notify_on_scroll<Message>( } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - Widget::<Message, Renderer>::width(&self.content) - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - Widget::<Message, Renderer>::width(self), - self.height, - self.max_height, - |renderer, limits| self.content.layout(renderer, limits), - ) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - self.state, - event, - layout, - cursor_position, - clipboard, - shell, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { - self.content.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - self.state, - layout, - cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - |layout, cursor_position, viewport| { - self.content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - self.state, - renderer, - theme, - layout, - cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - self.style, - |renderer, layout, cursor_position, viewport| { - self.content.draw( - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - let Self { content, state, .. } = self; - - content - .overlay(layout.children().next().unwrap(), renderer) - .map(|overlay| { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = state.offset(bounds, content_bounds); - - overlay.translate(Vector::new(0.0, -(offset as f32))) - }) - } -} - /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] pub struct State { @@ -782,6 +827,12 @@ impl Default for State { } } +impl operation::Scrollable for State { + fn snap_to(&mut self, percentage: f32) { + State::snap_to(self, percentage); + } +} + /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { @@ -926,17 +977,3 @@ struct Scroller { /// The bounds of the [`Scroller`]. bounds: Rectangle, } - -impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - scrollable: Scrollable<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(scrollable) - } -} diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index a5ff611c..585d9c35 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -6,6 +6,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::touch; +use crate::widget::tree::{self, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget, @@ -29,15 +30,15 @@ pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; /// # use iced_native::renderer::Null; /// # /// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; +/// # /// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } /// -/// let state = &mut slider::State::new(); /// let value = 50.0; /// -/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); +/// Slider::new(0.0..=100.0, value, Message::SliderChanged); /// ``` /// ///  @@ -47,11 +48,10 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, range: RangeInclusive<T>, step: T, value: T, - on_change: Box<dyn Fn(T) -> Message>, + on_change: Box<dyn Fn(T) -> Message + 'a>, on_release: Option<Message>, width: Length, height: u16, @@ -71,20 +71,14 @@ where /// Creates a new [`Slider`]. /// /// It expects: - /// * the local [`State`] of the [`Slider`] /// * 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`. - pub fn new<F>( - state: &'a mut State, - range: RangeInclusive<T>, - value: T, - on_change: F, - ) -> Self + pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where - F: 'static + Fn(T) -> Message, + F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { value @@ -99,7 +93,6 @@ where }; Slider { - state, value, range, step: T::from(1), @@ -150,6 +143,120 @@ where } } +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = + limits.width(self.width).height(Length::Units(self.height)); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::<State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + ) + } +} + +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + slider: Slider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} + /// Processes an [`Event`] and updates the [`State`] of a [`Slider`] /// accordingly. pub fn update<Message, T>( @@ -366,102 +473,3 @@ impl State { State::default() } } - -impl<'a, T, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, T, Message, Renderer> -where - T: Copy + Into<f64> + num_traits::FromPrimitive, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = - limits.width(self.width).height(Length::Units(self.height)); - - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - event, - layout, - cursor_position, - shell, - self.state, - &mut self.value, - &self.range, - self.step, - self.on_change.as_ref(), - &self.on_release, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - layout, - cursor_position, - self.state, - self.value, - &self.range, - theme, - self.style, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.state) - } -} - -impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> - for Element<'a, Message, Renderer> -where - T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - slider: Slider<'a, T, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(slider) - } -} diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index 81338306..9f835893 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -1,6 +1,7 @@ //! Distribute content vertically. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget}; /// An amount of empty space. @@ -59,6 +60,7 @@ where fn draw( &self, + _state: &Tree, _renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 76b3eb8b..aa68bfb8 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,13 +1,16 @@ //! Display vector graphics in your application. use crate::layout; use crate::renderer; -use crate::svg::{self, Handle}; +use crate::svg; +use crate::widget::Tree; use crate::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::path::PathBuf; +pub use svg::Handle; + /// A vector graphics image. /// /// An [`Svg`] image resizes smoothly without losing any quality. @@ -109,6 +112,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 8d173b8a..b30f4518 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -3,6 +3,7 @@ use crate::alignment; use crate::layout; use crate::renderer; use crate::text; +use crate::widget::Tree; use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget}; pub use iced_style::text::{Appearance, StyleSheet}; @@ -44,9 +45,9 @@ where Renderer::Theme: StyleSheet, { /// Create a new fragment of [`Text`] with the given contents. - pub fn new<T: Into<String>>(label: T) -> Self { + pub fn new<T: ToString>(label: T) -> Self { Text { - content: label.into(), + content: label.to_string(), size: None, font: Default::default(), width: Length::Shrink, @@ -145,6 +146,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -243,3 +245,13 @@ where } } } + +impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(contents: &'a str) -> Self { + Text::new(contents).into() + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index fd360cd7..8ddbc734 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -19,9 +19,12 @@ use crate::mouse::{self, click}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget; +use crate::widget::operation::{self, Operation}; +use crate::widget::tree::{self, Tree}; use crate::{ - Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, Color, Command, Element, Layout, Length, Padding, Point, + Rectangle, Shell, Size, Vector, Widget, }; pub use iced_style::text_input::{Appearance, StyleSheet}; @@ -30,20 +33,15 @@ pub use iced_style::text_input::{Appearance, StyleSheet}; /// /// # Example /// ``` -/// # use iced_native::renderer::Null; -/// # use iced_native::widget::text_input; -/// # -/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, Null>; +/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, iced_native::renderer::Null>; /// #[derive(Debug, Clone)] /// enum Message { /// TextInputChanged(String), /// } /// -/// let mut state = text_input::State::new(); /// let value = "Some text"; /// /// let input = TextInput::new( -/// &mut state, /// "This is the placeholder...", /// value, /// Message::TextInputChanged, @@ -57,7 +55,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, + id: Option<Id>, placeholder: String, value: Value, is_secure: bool, @@ -80,21 +78,15 @@ where /// Creates a new [`TextInput`]. /// /// It expects: - /// - some [`State`] - /// - a placeholder - /// - the current value - /// - a function that produces a message when the [`TextInput`] changes - pub fn new<F>( - state: &'a mut State, - placeholder: &str, - value: &str, - on_change: F, - ) -> Self + /// - a placeholder, + /// - the current value, and + /// - a function that produces a message when the [`TextInput`] changes. + pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self where F: 'a + Fn(String) -> Message, { TextInput { - state, + id: None, placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, @@ -109,6 +101,12 @@ where } } + /// Sets the [`Id`] of the [`TextInput`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Converts the [`TextInput`] into a secure password input. pub fn password(mut self) -> Self { self.is_secure = true; @@ -127,7 +125,7 @@ where /// Sets the [`Font`] of the [`TextInput`]. /// - /// [`Font`]: crate::text::Renderer::Font + /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self @@ -166,17 +164,13 @@ where self } - /// Returns the current [`State`] of the [`TextInput`]. - pub fn state(&self) -> &State { - self.state - } - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. + /// [`text_input::Value`] if provided. /// /// [`Renderer`]: text::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, @@ -188,7 +182,7 @@ where theme, layout, cursor_position, - self.state, + tree.state.downcast_ref::<State>(), value.unwrap_or(&self.value), &self.placeholder, self.size, @@ -199,6 +193,150 @@ where } } +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout(renderer, limits, self.width, self.padding, self.size) + } + + fn operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); + + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + &self.font, + self.is_secure, + self.on_change.as_ref(), + self.on_paste.as_deref(), + &self.on_submit, + || tree.state.downcast_mut::<State>(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + &self.value, + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } +} + +impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: TextInput<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`TextInput`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::focusable::focus(id.0)) +} + /// Computes the layout of a [`TextInput`]. pub fn layout<Renderer>( renderer: &Renderer, @@ -777,93 +915,6 @@ pub fn mouse_interaction( } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout(renderer, limits, self.width, self.padding, self.size) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - &mut self.value, - self.size, - &self.font, - self.is_secure, - self.on_change.as_ref(), - self.on_paste.as_deref(), - &self.on_submit, - || &mut self.state, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - self.draw(renderer, theme, layout, cursor_position, None) - } -} - -impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: TextInput<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} - /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State { @@ -907,6 +958,7 @@ impl State { /// Focuses the [`TextInput`]. pub fn focus(&mut self) { self.is_focused = true; + self.move_cursor_to_end(); } /// Unfocuses the [`TextInput`]. @@ -935,6 +987,20 @@ impl State { } } +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} + mod platform { use crate::keyboard; diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 3deaf287..7893f78c 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -5,7 +5,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::text; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Widget, @@ -180,6 +180,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -205,6 +206,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -219,6 +221,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs index 0548e9da..d8198004 100644 --- a/native/src/widget/tooltip.rs +++ b/native/src/widget/tooltip.rs @@ -6,7 +6,8 @@ use crate::renderer; use crate::text; use crate::widget; use crate::widget::container; -use crate::widget::text::Text; +use crate::widget::overlay; +use crate::widget::{Text, Tree}; use crate::{ Clipboard, Element, Event, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, @@ -14,7 +15,7 @@ use crate::{ /// An element to display a widget over another. #[allow(missing_debug_implementations)] -pub struct Tooltip<'a, Message, Renderer> +pub struct Tooltip<'a, Message, Renderer: text::Renderer> where Renderer: text::Renderer, Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, @@ -89,6 +90,153 @@ where } } +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Tooltip<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(renderer, limits) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + ); + + let tooltip = &self.tooltip; + + draw( + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + self.position, + self.gap, + self.padding, + self.style, + |renderer, limits| { + Widget::<(), Renderer>::layout(tooltip, renderer, limits) + }, + |renderer, defaults, layout, cursor_position, viewport| { + Widget::<(), Renderer>::draw( + tooltip, + &Tree::empty(), + renderer, + theme, + defaults, + layout, + cursor_position, + viewport, + ); + }, + ); + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + fn from( + tooltip: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(tooltip) + } +} + /// The position of the tooltip. Defaults to following the cursor. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Position { @@ -220,122 +368,3 @@ pub fn draw<Renderer>( }); } } - -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Tooltip<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - fn width(&self) -> Length { - self.content.width() - } - - fn height(&self) -> Length { - self.content.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.content.layout(renderer, limits) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.content.draw( - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - ); - - let tooltip = &self.tooltip; - - draw( - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - self.position, - self.gap, - self.padding, - self.style, - |renderer, limits| { - Widget::<(), Renderer>::layout(tooltip, renderer, limits) - }, - |renderer, defaults, layout, cursor_position, viewport| { - Widget::<(), Renderer>::draw( - tooltip, - renderer, - theme, - defaults, - layout, - cursor_position, - viewport, - ); - }, - ) - } -} - -impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - fn from( - tooltip: Tooltip<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(tooltip) - } -} diff --git a/native/src/widget/tree.rs b/native/src/widget/tree.rs new file mode 100644 index 00000000..a8b1a185 --- /dev/null +++ b/native/src/widget/tree.rs @@ -0,0 +1,187 @@ +//! Store internal widget state in a state tree to ensure continuity. +use crate::Widget; + +use std::any::{self, Any}; +use std::borrow::Borrow; +use std::fmt; + +/// A persistent state widget tree. +/// +/// A [`Tree`] is normally associated with a specific widget in the widget tree. +#[derive(Debug)] +pub struct Tree { + /// The tag of the [`Tree`]. + pub tag: Tag, + + /// The [`State`] of the [`Tree`]. + pub state: State, + + /// The children of the root widget of the [`Tree`]. + pub children: Vec<Tree>, +} + +impl Tree { + /// Creates an empty, stateless [`Tree`] with no children. + pub fn empty() -> Self { + Self { + tag: Tag::stateless(), + state: State::None, + children: Vec::new(), + } + } + + /// Creates a new [`Tree`] for the provided [`Element`]. + pub fn new<'a, Message, Renderer>( + widget: impl Borrow<dyn Widget<Message, Renderer> + 'a>, + ) -> Self + where + Renderer: crate::Renderer, + { + let widget = widget.borrow(); + + Self { + tag: widget.tag(), + state: widget.state(), + children: widget.children(), + } + } + + /// Reconciliates the current tree with the provided [`Element`]. + /// + /// If the tag of the [`Element`] matches the tag of the [`Tree`], then the + /// [`Element`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called). + /// + /// Otherwise, the whole [`Tree`] is recreated. + /// + /// [`Widget::diff`]: crate::Widget::diff + pub fn diff<'a, Message, Renderer>( + &mut self, + new: impl Borrow<dyn Widget<Message, Renderer> + 'a>, + ) where + Renderer: crate::Renderer, + { + if self.tag == new.borrow().tag() { + new.borrow().diff(self) + } else { + *self = Self::new(new); + } + } + + /// Reconciliates the children of the tree with the provided list of [`Element`]. + pub fn diff_children<'a, Message, Renderer>( + &mut self, + new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>], + ) where + Renderer: crate::Renderer, + { + self.diff_children_custom( + new_children, + |tree, widget| tree.diff(widget.borrow()), + |widget| Self::new(widget.borrow()), + ) + } + + /// Reconciliates the children of the tree with the provided list of [`Element`] using custom + /// logic both for diffing and creating new widget state. + pub fn diff_children_custom<T>( + &mut self, + new_children: &[T], + diff: impl Fn(&mut Tree, &T), + new_state: impl Fn(&T) -> Self, + ) { + if self.children.len() > new_children.len() { + self.children.truncate(new_children.len()); + } + + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) + { + diff(child_state, new); + } + + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(new_state), + ); + } + } +} + +/// The identifier of some widget state. +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { + /// Creates a [`Tag`] for a state of type `T`. + pub fn of<T>() -> Self + where + T: 'static, + { + Self(any::TypeId::of::<T>()) + } + + /// Creates a [`Tag`] for a stateless widget. + pub fn stateless() -> Self { + Self::of::<()>() + } +} + +/// The internal [`State`] of a widget. +pub enum State { + /// No meaningful internal state. + None, + + /// Some meaningful internal state. + Some(Box<dyn Any>), +} + +impl State { + /// Creates a new [`State`]. + pub fn new<T>(state: T) -> Self + where + T: 'static, + { + State::Some(Box::new(state)) + } + + /// Downcasts the [`State`] to `T` and returns a reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_ref<T>(&self) -> &T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_ref().expect("Downcast widget state") + } + } + } + + /// Downcasts the [`State`] to `T` and returns a mutable reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_mut<T>(&mut self) -> &mut T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_mut().expect("Downcast widget state") + } + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "State::None"), + Self::Some(_) => write!(f, "State::Some"), + } + } +} |