From 6ab4611a6eec9c4bb4ca1ff1bb580bb7edf49add Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 11 Jan 2022 13:47:43 +0700 Subject: Invalidate widget tree from `Responsive` widget ... by introducing a new `invalidate_widgets` method to `Shell` --- lazy/src/component.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lazy/src') diff --git a/lazy/src/component.rs b/lazy/src/component.rs index fd3f6ff7..48ee1d87 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -149,7 +149,7 @@ where ) }); - local_shell.with_invalid_layout(|| shell.invalidate_layout()); + local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { let mut component = self @@ -375,7 +375,7 @@ where }) .unwrap_or_else(|| iced_native::event::Status::Ignored); - local_shell.with_invalid_layout(|| shell.invalidate_layout()); + local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { let mut component = -- cgit From 810e086728e938d1d12758c7b486c1e371127349 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 11 Jan 2022 14:12:28 +0700 Subject: Introduce `Renderer` argument to `mouse_interaction` and `on_event` --- lazy/src/component.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'lazy/src') diff --git a/lazy/src/component.rs b/lazy/src/component.rs index 48ee1d87..836c3f01 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -214,15 +214,22 @@ where layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, + renderer: &Renderer, ) -> mouse::Interaction { self.with_element(|element| { - element.mouse_interaction(layout, cursor_position, viewport) + element.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) }) } fn overlay( &mut self, layout: Layout<'_>, + renderer: &Renderer, ) -> Option> { let has_overlay = self .state @@ -236,7 +243,9 @@ where CacheBuilder { element, message: PhantomData, - overlay_builder: |element| element.overlay(layout), + overlay_builder: |element| { + element.overlay(layout, renderer) + }, } .build(), ); @@ -331,9 +340,15 @@ where layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, + renderer: &Renderer, ) -> mouse::Interaction { self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction(layout, cursor_position, viewport) + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) }) .unwrap_or_default() } @@ -397,7 +412,7 @@ where element: state.view(), message: PhantomData, overlay_builder: |element| { - element.overlay(layout) + element.overlay(layout, renderer) }, } .build(), -- cgit From 5a03cac7e75ccb8ca87a97def723694be0471742 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 11 Jan 2022 14:55:48 +0700 Subject: Remove superfluous generic type in `iced_lazy::Cache` --- lazy/src/component.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'lazy/src') diff --git a/lazy/src/component.rs b/lazy/src/component.rs index 836c3f01..ca0d115c 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -24,11 +24,11 @@ where state: RefCell::new(Some( StateBuilder { component: Box::new(component), + message: PhantomData, cache_builder: |state| { Some( CacheBuilder { element: state.view(), - message: PhantomData, overlay_builder: |_| None, } .build(), @@ -55,20 +55,20 @@ struct Instance<'a, Message, Renderer, Event> { #[self_referencing] struct State<'a, Message: 'a, Renderer: 'a, Event: 'a> { component: Box + 'a>, + message: PhantomData, #[borrows(mut component)] #[covariant] - cache: Option>, + cache: Option>, } #[self_referencing] -struct Cache<'a, Message, Renderer: 'a, Event: 'a> { - element: Element<'a, Event, Renderer>, - message: PhantomData, +struct Cache<'a, Message: 'a, Renderer: 'a> { + element: Element<'a, Message, Renderer>, #[borrows(mut element)] #[covariant] - overlay: Option>, + overlay: Option>, } impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { @@ -94,7 +94,6 @@ impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { *cache = Some( CacheBuilder { element, - message: PhantomData, overlay_builder: |_| None, } .build(), @@ -170,11 +169,11 @@ where *self.state.borrow_mut() = Some( StateBuilder { component, + message: PhantomData, cache_builder: |state| { Some( CacheBuilder { element: state.view(), - message: PhantomData, overlay_builder: |_| None, } .build(), @@ -242,7 +241,6 @@ where *cache = Some( CacheBuilder { element, - message: PhantomData, overlay_builder: |element| { element.overlay(layout, renderer) }, @@ -406,11 +404,11 @@ where self.instance.state = RefCell::new(Some( StateBuilder { component, + message: PhantomData, cache_builder: |state| { Some( CacheBuilder { element: state.view(), - message: PhantomData, overlay_builder: |element| { element.overlay(layout, renderer) }, -- cgit From 870d651f35c4dad12c805951fca70213816983de Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jan 2022 11:15:05 +0700 Subject: Implement `Widget::overlay` for `Responsive` widget --- lazy/src/cache.rs | 13 ++ lazy/src/component.rs | 11 +- lazy/src/lib.rs | 6 + lazy/src/responsive.rs | 413 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 434 insertions(+), 9 deletions(-) create mode 100644 lazy/src/cache.rs create mode 100644 lazy/src/responsive.rs (limited to 'lazy/src') diff --git a/lazy/src/cache.rs b/lazy/src/cache.rs new file mode 100644 index 00000000..229b7912 --- /dev/null +++ b/lazy/src/cache.rs @@ -0,0 +1,13 @@ +use iced_native::overlay; +use iced_native::Element; + +use ouroboros::self_referencing; + +#[self_referencing(pub_extras)] +pub struct Cache<'a, Message: 'a, Renderer: 'a> { + pub element: Element<'a, Message, Renderer>, + + #[borrows(mut element)] + #[covariant] + pub overlay: Option>, +} diff --git a/lazy/src/component.rs b/lazy/src/component.rs index ca0d115c..ae8d6bbe 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -1,3 +1,5 @@ +use crate::{Cache, CacheBuilder}; + use iced_native::event; use iced_native::layout::{self, Layout}; use iced_native::mouse; @@ -62,15 +64,6 @@ struct State<'a, Message: 'a, Renderer: 'a, Event: 'a> { cache: Option>, } -#[self_referencing] -struct Cache<'a, Message: 'a, Renderer: 'a> { - element: Element<'a, Message, Renderer>, - - #[borrows(mut element)] - #[covariant] - overlay: Option>, -} - impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { fn with_element( &self, diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index 42e5f587..05fce765 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -1,3 +1,9 @@ pub mod component; +pub mod responsive; pub use component::Component; +pub use responsive::Responsive; + +mod cache; + +use cache::{Cache, CacheBuilder}; diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs new file mode 100644 index 00000000..2afad2e5 --- /dev/null +++ b/lazy/src/responsive.rs @@ -0,0 +1,413 @@ +use crate::{Cache, CacheBuilder}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::{ + Clipboard, Element, Hasher, Length, Point, Rectangle, Shell, Size, Widget, +}; + +use std::cell::RefCell; +use std::hash::{Hash, Hasher as _}; +use std::ops::Deref; + +#[derive(Debug, Clone, Default)] +pub struct State { + last_size: Option, + last_layout: layout::Node, + last_layout_hash: u64, +} + +impl State { + pub fn new() -> State { + State::default() + } + + pub fn layout(&self, parent: Layout<'_>) -> Layout<'_> { + Layout::with_offset( + parent.position() - Point::ORIGIN, + &self.last_layout, + ) + } +} + +#[allow(missing_debug_implementations)] +pub struct Responsive<'a, Message, Renderer>( + RefCell>, +); + +impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> { + pub fn new( + state: &'a mut State, + view: impl FnOnce(Size) -> Element<'a, Message, Renderer> + 'a, + ) -> Self { + Self(RefCell::new(Internal { + state, + content: Content::Pending(Some(Box::new(view))), + })) + } +} + +impl<'a, Message, Renderer> Widget + for Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Fill + } + + fn hash_layout(&self, _hasher: &mut Hasher) {} + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = limits.max(); + + self.0.borrow_mut().state.last_size = Some(size); + + 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 { + let mut internal = self.0.borrow_mut(); + + if internal.state.last_size != Some(internal.state.last_layout.size()) { + shell.invalidate_widgets(); + } + + internal.resolve(renderer, |state, renderer, content| { + content.on_event( + event, + state.layout(layout), + cursor_position, + renderer, + clipboard, + shell, + ) + }) + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let mut internal = self.0.borrow_mut(); + + internal.resolve(renderer, |state, renderer, content| { + content.draw( + renderer, + style, + state.layout(layout), + cursor_position, + viewport, + ) + }) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let mut internal = self.0.borrow_mut(); + + internal.resolve(renderer, |state, renderer, content| { + content.mouse_interaction( + state.layout(layout), + cursor_position, + viewport, + renderer, + ) + }) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let has_overlay = { + use std::ops::DerefMut; + + let mut internal = self.0.borrow_mut(); + + let _ = + internal.resolve(renderer, |_state, _renderer, _content| {}); + + let Internal { content, state } = internal.deref_mut(); + + let content_layout = state.layout(layout); + + match content { + Content::Pending(_) => false, + Content::Ready(cache) => { + *cache = Some( + CacheBuilder { + element: cache.take().unwrap().into_heads().element, + overlay_builder: |element| { + element.overlay(content_layout, renderer) + }, + } + .build(), + ); + + cache.as_ref().unwrap().borrow_overlay().is_some() + } + } + }; + + has_overlay.then(|| { + overlay::Element::new( + layout.position(), + Box::new(Overlay { instance: self }), + ) + }) + } +} + +struct Internal<'a, Message, Renderer> { + state: &'a mut State, + content: Content<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Internal<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn resolve( + &mut self, + renderer: R, + f: impl FnOnce(&State, R, &mut Element<'a, Message, Renderer>) -> T, + ) -> T + where + R: Deref, + { + self.content.resolve(&mut self.state, renderer, f) + } +} + +enum Content<'a, Message, Renderer> { + Pending( + Option Element<'a, Message, Renderer> + 'a>>, + ), + Ready(Option>), +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn resolve( + &mut self, + state: &mut State, + renderer: R, + f: impl FnOnce(&State, R, &mut Element<'a, Message, Renderer>) -> T, + ) -> T + where + R: Deref, + { + match self { + Content::Ready(cache) => { + let mut heads = cache.take().unwrap().into_heads(); + + let result = f(state, renderer, &mut heads.element); + + *cache = Some( + CacheBuilder { + element: heads.element, + overlay_builder: |_| None, + } + .build(), + ); + + result + } + Content::Pending(view) => { + let element = + view.take().unwrap()(state.last_size.unwrap_or(Size::ZERO)); + + let new_layout_hash = { + let mut hasher = Hasher::default(); + element.hash_layout(&mut hasher); + + hasher.finish() + }; + + if new_layout_hash != state.last_layout_hash { + state.last_layout = element.layout( + renderer.deref(), + &layout::Limits::new( + Size::ZERO, + state.last_size.unwrap_or(Size::ZERO), + ), + ); + + state.last_layout_hash = new_layout_hash; + } + + *self = Content::Ready(Some( + CacheBuilder { + element, + overlay_builder: |_| None, + } + .build(), + )); + + self.resolve(state, renderer, f) + } + } + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: iced_native::Renderer + 'a, + Message: 'a, +{ + fn from(responsive: Responsive<'a, Message, Renderer>) -> Self { + Self::new(responsive) + } +} + +struct Overlay<'a, 'b, Message, Renderer> { + instance: &'b mut Responsive<'a, Message, Renderer>, +} + +impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { + fn with_overlay_maybe( + &self, + f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option { + let internal = self.instance.0.borrow(); + + match &internal.content { + Content::Pending(_) => None, + Content::Ready(cache) => { + cache.as_ref().unwrap().borrow_overlay().as_ref().map(f) + } + } + } + + fn with_overlay_mut_maybe( + &self, + f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option { + let mut internal = self.instance.0.borrow_mut(); + + match &mut internal.content { + Content::Pending(_) => None, + Content::Ready(cache) => cache + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)), + } + } +} + +impl<'a, 'b, Message, Renderer> overlay::Overlay + for Overlay<'a, 'b, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + 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::().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 { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .unwrap_or_else(|| iced_native::event::Status::Ignored) + } +} -- cgit From 241e123c9b49bb5f8c6bf223eef666c94042dd8f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jan 2022 11:39:54 +0700 Subject: Write documentation for `component` in `iced_lazy` --- lazy/src/component.rs | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) (limited to 'lazy/src') diff --git a/lazy/src/component.rs b/lazy/src/component.rs index ae8d6bbe..2b729045 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -1,3 +1,4 @@ +//! Build and reuse custom widgets using The Elm Architecture. use crate::{Cache, CacheBuilder}; use iced_native::event; @@ -14,6 +15,35 @@ use std::cell::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 { + /// 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, event: Self::Event) -> Option; + + /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) + /// on user interaction. + fn view(&mut self) -> Element; +} + +/// 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> @@ -42,14 +72,6 @@ where }) } -pub trait Component { - type Event; - - fn update(&mut self, event: Self::Event) -> Option; - - fn view(&mut self) -> Element; -} - struct Instance<'a, Message, Renderer, Event> { state: RefCell>>, } -- cgit From f6c436aec1acb674078bf7e878b9e49f28e947a7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 12 Jan 2022 11:48:49 +0700 Subject: Write docs for `responsive` in `iced_lazy` --- lazy/src/responsive.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'lazy/src') diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs index 2afad2e5..da7bb408 100644 --- a/lazy/src/responsive.rs +++ b/lazy/src/responsive.rs @@ -1,3 +1,4 @@ +//! Build responsive widgets. use crate::{Cache, CacheBuilder}; use iced_native::event::{self, Event}; @@ -13,6 +14,7 @@ use std::cell::RefCell; use std::hash::{Hash, Hasher as _}; use std::ops::Deref; +/// The state of a [`Responsive`] widget. #[derive(Debug, Clone, Default)] pub struct State { last_size: Option, @@ -25,7 +27,7 @@ impl State { State::default() } - pub fn layout(&self, parent: Layout<'_>) -> Layout<'_> { + fn layout(&self, parent: Layout<'_>) -> Layout<'_> { Layout::with_offset( parent.position() - Point::ORIGIN, &self.last_layout, @@ -33,12 +35,22 @@ impl State { } } +/// A widget that is aware of its dimensions. +/// +/// A [`Responsive`] widget will always try to fill all the available space of +/// its parent. #[allow(missing_debug_implementations)] pub struct Responsive<'a, Message, Renderer>( RefCell>, ); impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> { + /// Creates a new [`Responsive`] widget with the given [`State`] and a + /// closure that produces its contents. + /// + /// The `view` closure will be provided with the current [`Size`] of + /// the [`Responsive`] widget and, therefore, can be used to build the + /// contents of the widget in a responsive way. pub fn new( state: &'a mut State, view: impl FnOnce(Size) -> Element<'a, Message, Renderer> + 'a, -- cgit