diff options
author | 2022-02-17 19:08:54 +0700 | |
---|---|---|
committer | 2022-02-17 19:08:54 +0700 | |
commit | da45b6c1627935bff5334d213096c4e78972af46 (patch) | |
tree | 86840f251dd51f41de2fe79083e9975b51ec1f4f | |
parent | 2737b21d346c9034502c345dea735ca136b49cea (diff) | |
download | iced-da45b6c1627935bff5334d213096c4e78972af46.tar.gz iced-da45b6c1627935bff5334d213096c4e78972af46.tar.bz2 iced-da45b6c1627935bff5334d213096c4e78972af46.zip |
Implement `pure::Component` in `iced_lazy`
-rw-r--r-- | lazy/Cargo.toml | 8 | ||||
-rw-r--r-- | lazy/src/lib.rs | 3 | ||||
-rw-r--r-- | lazy/src/pure.rs | 1 | ||||
-rw-r--r-- | lazy/src/pure/component.rs | 500 | ||||
-rw-r--r-- | pure/src/overlay.rs | 6 | ||||
-rw-r--r-- | pure/src/widget.rs | 4 | ||||
-rw-r--r-- | pure/src/widget/button.rs | 4 | ||||
-rw-r--r-- | pure/src/widget/column.rs | 4 | ||||
-rw-r--r-- | pure/src/widget/container.rs | 4 | ||||
-rw-r--r-- | pure/src/widget/pick_list.rs | 2 | ||||
-rw-r--r-- | pure/src/widget/row.rs | 4 | ||||
-rw-r--r-- | pure/src/widget/scrollable.rs | 4 | ||||
-rw-r--r-- | pure/src/widget/space.rs | 7 |
13 files changed, 530 insertions, 21 deletions
diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index b840de50..2d7451f3 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -3,9 +3,17 @@ name = "iced_lazy" version = "0.1.0" edition = "2021" +[features] +pure = ["iced_pure"] + [dependencies] ouroboros = "0.13" [dependencies.iced_native] version = "0.4" path = "../native" + +[dependencies.iced_pure] +version = "0.1" +path = "../pure" +optional = true diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index 05fce765..5d7d10e4 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -1,6 +1,9 @@ pub mod component; pub mod responsive; +#[cfg(feature = "pure")] +pub mod pure; + pub use component::Component; pub use responsive::Responsive; diff --git a/lazy/src/pure.rs b/lazy/src/pure.rs new file mode 100644 index 00000000..9cea807e --- /dev/null +++ b/lazy/src/pure.rs @@ -0,0 +1 @@ +pub mod component; diff --git a/lazy/src/pure/component.rs b/lazy/src/pure/component.rs new file mode 100644 index 00000000..3061e45d --- /dev/null +++ b/lazy/src/pure/component.rs @@ -0,0 +1,500 @@ +//! Build and reuse custom widgets using The Elm Architecture. +use iced_native::event; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size}; +use iced_pure::widget::tree::{self, Tree}; +use iced_pure::{Element, Widget}; + +use ouroboros::self_referencing; +use std::cell::{Ref, RefCell}; +use std::hash::Hash; +use std::marker::PhantomData; + +/// A reusable, custom widget that uses The Elm Architecture. +/// +/// A [`Component`] allows you to implement custom widgets as if they were +/// `iced` applications with encapsulated state. +/// +/// In other words, a [`Component`] allows you to turn `iced` applications into +/// custom widgets and embed them without cumbersome wiring. +/// +/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) +/// and update the internal state of the [`Component`]. +/// +/// Additionally, a [`Component`] is capable of producing a `Message` to notify +/// the parent application of any relevant interactions. +pub trait Component<Message, Renderer> { + /// The internal state of this [`Component`]. + type State: Default; + + /// The type of event this [`Component`] handles internally. + type Event; + + /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. + /// + /// It can produce a `Message` for the parent application. + fn update( + &mut self, + state: &mut Self::State, + event: Self::Event, + ) -> Option<Message>; + + /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) + /// on user interaction. + fn view(&self, state: &Self::State) -> Element<Self::Event, Renderer>; +} + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn view<'a, C, Message, Renderer>( + component: C, +) -> Element<'a, Message, Renderer> +where + C: Component<Message, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Renderer: iced_native::Renderer + 'a, +{ + Element::new(Instance { + state: RefCell::new(Some( + StateBuilder { + component: Box::new(component), + message: PhantomData, + state: PhantomData, + element_builder: |_| None, + } + .build(), + )), + }) +} + +struct Instance<'a, Message, Renderer, Event, S> { + state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, +} + +#[self_referencing] +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { + component: + Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>, + message: PhantomData<Message>, + state: PhantomData<S>, + + #[borrows(component)] + #[covariant] + element: Option<Element<'this, Event, Renderer>>, +} + +impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> +where + S: Default, +{ + fn with_element<T>( + &self, + f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, + ) -> T { + self.with_element_mut(|element| f(element)) + } + + fn with_element_mut<T>( + &self, + f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, + ) -> T { + if self + .state + .borrow() + .as_ref() + .unwrap() + .borrow_element() + .is_none() + { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| Some(state.view(&S::default())), + } + .build(), + ); + } + + self.state + .borrow_mut() + .as_mut() + .unwrap() + .with_element_mut(|element| f(element.as_mut().unwrap())) + } +} + +impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer> + for Instance<'a, Message, Renderer, Event, S> +where + S: 'static + Default, + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<S>() + } + + fn state(&self) -> tree::State { + tree::State::new(S::default()) + } + + fn children(&self) -> Vec<Tree> { + self.with_element(|element| vec![Tree::new(element)]) + } + + fn diff(&self, tree: &mut Tree) { + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }) + } + + fn width(&self) -> Length { + self.with_element(|element| element.as_widget().width()) + } + + fn height(&self) -> Length { + self.with_element(|element| element.as_widget().height()) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.with_element(|element| { + element.as_widget().layout(renderer, limits) + }) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self.with_element_mut(|element| { + element.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ) + }); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if !local_messages.is_empty() { + let mut heads = self.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(tree.state.downcast_mut::<S>(), message) + }) { + shell.publish(message); + } + + self.state = RefCell::new(Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| { + Some(state.view(tree.state.downcast_ref::<S>())) + }, + } + .build(), + )); + + shell.invalidate_layout(); + } + + event_status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.with_element(|element| { + element.as_widget().draw( + &tree.children[0], + renderer, + style, + layout, + cursor_position, + viewport, + ); + }); + } + + fn hash_layout(&self, state: &mut Hasher) { + self.with_element(|element| { + element.as_widget().hash_layout(state); + }); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_element(|element| { + element.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>> { + let overlay = OverlayBuilder { + instance: self, + instance_ref_builder: |instance| instance.state.borrow(), + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance + .as_ref() + .unwrap() + .borrow_element() + .as_ref() + .unwrap() + .as_widget() + .overlay(&mut tree.children[0], layout, renderer) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay.map(|position| { + overlay::Element::new( + position, + Box::new(OverlayInstance { + overlay: Some(overlay), + }), + ) + }) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer, Event, S> { + instance: &'a Instance<'b, Message, Renderer, Event, S>, + tree: &'a mut Tree, + types: PhantomData<(Message, Event, S)>, + + #[borrows(instance)] + #[covariant] + instance_ref: Ref<'this, Option<State<'a, Message, Renderer, Event, S>>>, + + #[borrows(instance_ref, mut tree)] + #[covariant] + overlay: Option<overlay::Element<'this, Event, Renderer>>, +} + +struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { + overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>, +} + +impl<'a, 'b, Message, Renderer, Event, S> + OverlayInstance<'a, 'b, Message, Renderer, Event, S> +{ + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_ref() + .unwrap() + .borrow_overlay() + .as_ref() + .map(f) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer> + for OverlayInstance<'a, 'b, Message, Renderer, Event, S> +where + Renderer: iced_native::Renderer, + S: 'static + Default, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + let vector = position - overlay.position(); + + overlay.layout(renderer, bounds).translate(vector) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + (position.x as u32).hash(state); + (position.y as u32).hash(state); + + self.with_overlay_maybe(|overlay| { + overlay.hash_layout(state); + }); + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> iced_native::event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self + .with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ) + }) + .unwrap_or_else(|| iced_native::event::Status::Ignored); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if !local_messages.is_empty() { + let overlay = self.overlay.take().unwrap().into_heads(); + let mut heads = overlay.instance.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(overlay.tree.state.downcast_mut::<S>(), message) + }) { + shell.publish(message); + } + + *overlay.instance.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| { + Some(state.view(overlay.tree.state.downcast_ref::<S>())) + }, + } + .build(), + ); + + self.overlay = Some( + OverlayBuilder { + instance: overlay.instance, + instance_ref_builder: |instance| instance.state.borrow(), + tree: overlay.tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance + .as_ref() + .unwrap() + .borrow_element() + .as_ref() + .unwrap() + .as_widget() + .overlay(tree, layout, renderer) + }, + } + .build(), + ); + + shell.invalidate_layout(); + } + + event_status + } +} diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs index b009fde8..72415634 100644 --- a/pure/src/overlay.rs +++ b/pure/src/overlay.rs @@ -5,17 +5,17 @@ use iced_native::Layout; pub use iced_native::overlay::*; pub fn from_children<'a, Message, Renderer>( - children: &'a mut [crate::Element<'_, Message, Renderer>], + children: &'a [crate::Element<'_, Message, Renderer>], tree: &'a mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<Element<'a, Message, Renderer>> { children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) + child.as_widget().overlay(state, layout, renderer) }) .next() } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 62f9d95b..a12d6fad 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ pub mod image; +pub mod tree; mod button; mod checkbox; @@ -14,7 +15,6 @@ mod space; mod text; mod text_input; mod toggler; -mod tree; pub use button::Button; pub use checkbox::Checkbox; @@ -104,7 +104,7 @@ pub trait Widget<Message, Renderer> { } fn overlay<'a>( - &'a mut self, + &'a self, _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index f5e78933..2ed67a9c 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -209,12 +209,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 1f025335..698d7e9c 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -219,12 +219,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&self.children, tree, layout, renderer) } } diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index 8ad6a064..c8f0b3a2 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -240,12 +240,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, diff --git a/pure/src/widget/pick_list.rs b/pure/src/widget/pick_list.rs index 324950e1..9dc847ee 100644 --- a/pure/src/widget/pick_list.rs +++ b/pure/src/widget/pick_list.rs @@ -212,7 +212,7 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 29128589..1c574d51 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -205,12 +205,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&self.children, tree, layout, renderer) } } diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index 8a206a6c..bbda50e5 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -233,13 +233,13 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { self.content - .as_widget_mut() + .as_widget() .overlay( &mut tree.children[0], layout.children().next().unwrap(), diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs index d7394398..e0c9c285 100644 --- a/pure/src/widget/space.rs +++ b/pure/src/widget/space.rs @@ -4,15 +4,13 @@ use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::renderer; -use iced_native::text; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; pub use iced_native::widget::Space; impl<'a, Message, Renderer> Widget<Message, Renderer> for Space where - Message: Clone, - Renderer: text::Renderer, + Renderer: iced_native::Renderer, { fn width(&self) -> Length { <Self as iced_native::Widget<Message, Renderer>>::width(self) @@ -98,8 +96,7 @@ where impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Space where - Message: 'a + Clone, - Renderer: text::Renderer + 'a, + Renderer: iced_native::Renderer + 'a, { fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) |