Merge pull request #1400 from nicksenger/lazy/cached

`Lazy` widget
This commit is contained in:
Héctor Ramón 2022-11-03 03:12:23 +01:00 committed by GitHub
commit 231d2fd845
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 677 additions and 1 deletions

View file

@ -72,6 +72,7 @@ members = [
"examples/geometry",
"examples/integration_opengl",
"examples/integration_wgpu",
"examples/lazy",
"examples/multitouch",
"examples/pane_grid",
"examples/pick_list",

View file

@ -0,0 +1,10 @@
[package]
name = "cached"
version = "0.1.0"
authors = ["Nick Senger <dev@nsenger.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
iced_lazy = { path = "../../lazy" }

139
examples/cached/src/main.rs Normal file
View file

@ -0,0 +1,139 @@
use iced::theme;
use iced::widget::{
button, column, horizontal_space, row, scrollable, text, text_input,
};
use iced::{Element, Length, Sandbox, Settings};
use iced_lazy::lazy;
use std::collections::HashSet;
pub fn main() -> iced::Result {
App::run(Settings::default())
}
struct App {
options: HashSet<String>,
input: String,
order: Order,
}
impl Default for App {
fn default() -> Self {
Self {
options: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"]
.into_iter()
.map(ToString::to_string)
.collect(),
input: Default::default(),
order: Order::Ascending,
}
}
}
#[derive(Debug, Clone)]
enum Message {
InputChanged(String),
ToggleOrder,
DeleteOption(String),
AddOption(String),
}
impl Sandbox for App {
type Message = Message;
fn new() -> Self {
Self::default()
}
fn title(&self) -> String {
String::from("Cached - Iced")
}
fn update(&mut self, message: Message) {
match message {
Message::InputChanged(input) => {
self.input = input;
}
Message::ToggleOrder => {
self.order = match self.order {
Order::Ascending => Order::Descending,
Order::Descending => Order::Ascending,
}
}
Message::AddOption(option) => {
self.options.insert(option);
self.input.clear();
}
Message::DeleteOption(option) => {
self.options.remove(&option);
}
}
}
fn view(&self) -> Element<Message> {
let options = lazy((&self.order, self.options.len()), || {
let mut options: Vec<_> = self.options.iter().collect();
options.sort_by(|a, b| match self.order {
Order::Ascending => a.to_lowercase().cmp(&b.to_lowercase()),
Order::Descending => b.to_lowercase().cmp(&a.to_lowercase()),
});
column(
options
.into_iter()
.map(|option| {
row![
text(option),
horizontal_space(Length::Fill),
button("Delete")
.on_press(Message::DeleteOption(
option.to_string(),
),)
.style(theme::Button::Destructive)
]
.into()
})
.collect(),
)
.spacing(10)
});
column![
scrollable(options).height(Length::Fill),
row![
text_input(
"Add a new option",
&self.input,
Message::InputChanged,
)
.on_submit(Message::AddOption(self.input.clone())),
button(text(format!("Toggle Order ({})", self.order)))
.on_press(Message::ToggleOrder)
]
.spacing(10)
]
.spacing(20)
.padding(20)
.into()
}
}
#[derive(Debug, Hash)]
enum Order {
Ascending,
Descending,
}
impl std::fmt::Display for Order {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Ascending => "Ascending",
Self::Descending => "Descending",
}
)
}
}

10
examples/lazy/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "lazy"
version = "0.1.0"
authors = ["Nick Senger <dev@nsenger.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
iced_lazy = { path = "../../lazy" }

139
examples/lazy/src/main.rs Normal file
View file

@ -0,0 +1,139 @@
use iced::theme;
use iced::widget::{
button, column, horizontal_space, row, scrollable, text, text_input,
};
use iced::{Element, Length, Sandbox, Settings};
use iced_lazy::lazy;
use std::collections::HashSet;
pub fn main() -> iced::Result {
App::run(Settings::default())
}
struct App {
options: HashSet<String>,
input: String,
order: Order,
}
impl Default for App {
fn default() -> Self {
Self {
options: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"]
.into_iter()
.map(ToString::to_string)
.collect(),
input: Default::default(),
order: Order::Ascending,
}
}
}
#[derive(Debug, Clone)]
enum Message {
InputChanged(String),
ToggleOrder,
DeleteOption(String),
AddOption(String),
}
impl Sandbox for App {
type Message = Message;
fn new() -> Self {
Self::default()
}
fn title(&self) -> String {
String::from("Cached - Iced")
}
fn update(&mut self, message: Message) {
match message {
Message::InputChanged(input) => {
self.input = input;
}
Message::ToggleOrder => {
self.order = match self.order {
Order::Ascending => Order::Descending,
Order::Descending => Order::Ascending,
}
}
Message::AddOption(option) => {
self.options.insert(option);
self.input.clear();
}
Message::DeleteOption(option) => {
self.options.remove(&option);
}
}
}
fn view(&self) -> Element<Message> {
let options = lazy((&self.order, self.options.len()), || {
let mut options: Vec<_> = self.options.iter().collect();
options.sort_by(|a, b| match self.order {
Order::Ascending => a.to_lowercase().cmp(&b.to_lowercase()),
Order::Descending => b.to_lowercase().cmp(&a.to_lowercase()),
});
column(
options
.into_iter()
.map(|option| {
row![
text(option),
horizontal_space(Length::Fill),
button("Delete")
.on_press(Message::DeleteOption(
option.to_string(),
),)
.style(theme::Button::Destructive)
]
.into()
})
.collect(),
)
.spacing(10)
});
column![
scrollable(options).height(Length::Fill),
row![
text_input(
"Add a new option",
&self.input,
Message::InputChanged,
)
.on_submit(Message::AddOption(self.input.clone())),
button(text(format!("Toggle Order ({})", self.order)))
.on_press(Message::ToggleOrder)
]
.spacing(10)
]
.spacing(20)
.padding(20)
.into()
}
}
#[derive(Debug, Hash)]
enum Order {
Ascending,
Descending,
}
impl std::fmt::Display for Order {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Ascending => "Ascending",
Self::Descending => "Descending",
}
)
}
}

362
lazy/src/lazy.rs Normal file
View file

@ -0,0 +1,362 @@
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::widget::tree::{self, Tree};
use iced_native::widget::{self, Widget};
use iced_native::Element;
use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size};
use ouroboros::self_referencing;
use std::cell::{Ref, RefCell, RefMut};
use std::hash::{Hash, Hasher as H};
use std::marker::PhantomData;
use std::rc::Rc;
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Renderer, Dependency, View> {
dependency: Dependency,
view: Box<dyn Fn() -> View + 'a>,
element: RefCell<Option<Rc<RefCell<Element<'static, Message, Renderer>>>>>,
}
impl<'a, Message, Renderer, Dependency, View>
Lazy<'a, Message, Renderer, Dependency, View>
where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Renderer>>,
{
pub fn new(dependency: Dependency, view: impl Fn() -> View + 'a) -> Self {
Self {
dependency,
view: Box::new(view),
element: RefCell::new(None),
}
}
fn with_element<T>(
&self,
f: impl FnOnce(Ref<Element<Message, Renderer>>) -> T,
) -> T {
f(self.element.borrow().as_ref().unwrap().borrow())
}
fn with_element_mut<T>(
&self,
f: impl FnOnce(RefMut<Element<Message, Renderer>>) -> T,
) -> T {
f(self.element.borrow().as_ref().unwrap().borrow_mut())
}
}
struct Internal<Message, Renderer> {
element: Rc<RefCell<Element<'static, Message, Renderer>>>,
hash: u64,
}
impl<'a, Message, Renderer, Dependency, View> Widget<Message, Renderer>
for Lazy<'a, Message, Renderer, Dependency, View>
where
View: Into<Element<'static, Message, Renderer>> + 'static,
Dependency: Hash + 'a,
Message: 'static,
Renderer: iced_native::Renderer + 'static,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
tree::Tag::of::<Tag<View>>()
}
fn state(&self) -> tree::State {
let mut hasher = Hasher::default();
self.dependency.hash(&mut hasher);
let hash = hasher.finish();
let element = Rc::new(RefCell::new((self.view)().into()));
(*self.element.borrow_mut()) = Some(element.clone());
tree::State::new(Internal { element, hash })
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(
self.element.borrow().as_ref().unwrap().borrow().as_widget(),
)]
}
fn diff(&self, tree: &mut Tree) {
let current = tree.state.downcast_mut::<Internal<Message, Renderer>>();
let mut hasher = Hasher::default();
self.dependency.hash(&mut hasher);
let new_hash = hasher.finish();
if current.hash != new_hash {
current.hash = new_hash;
let element = (self.view)().into();
current.element = Rc::new(RefCell::new(element));
(*self.element.borrow_mut()) = Some(current.element.clone());
tree.diff_children(std::slice::from_ref(
&self.element.borrow().as_ref().unwrap().borrow().as_widget(),
));
} else {
(*self.element.borrow_mut()) = Some(current.element.clone());
}
}
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 operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
operation: &mut dyn widget::Operation<Message>,
) {
self.with_element(|element| {
element.as_widget().operate(
&mut tree.children[0],
layout,
operation,
);
});
}
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 {
self.with_element_mut(|mut element| {
element.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.with_element(|element| {
element.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor_position,
viewport,
renderer,
)
})
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
self.with_element(|element| {
element.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor_position,
viewport,
)
})
}
fn overlay<'b>(
&'b self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'_, Message, Renderer>> {
let overlay = OverlayBuilder {
cached: self,
tree: &mut tree.children[0],
types: PhantomData,
element_ref_builder: |cached| cached.element.borrow(),
element_builder: |element_ref| {
element_ref.as_ref().unwrap().borrow()
},
overlay_builder: |element, tree| {
element.as_widget().overlay(tree, 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(overlay)))
}
}
#[self_referencing]
struct Overlay<'a, 'b, Message, Renderer, Dependency, View> {
cached: &'a Lazy<'b, Message, Renderer, Dependency, View>,
tree: &'a mut Tree,
types: PhantomData<(Message, Dependency, View)>,
#[borrows(cached)]
#[covariant]
element_ref:
Ref<'this, Option<Rc<RefCell<Element<'static, Message, Renderer>>>>>,
#[borrows(element_ref)]
#[covariant]
element: Ref<'this, Element<'static, Message, Renderer>>,
#[borrows(element, mut tree)]
#[covariant]
overlay: Option<overlay::Element<'this, Message, Renderer>>,
}
impl<'a, 'b, Message, Renderer, Dependency, View>
Overlay<'a, 'b, Message, Renderer, Dependency, View>
{
fn with_overlay_maybe<T>(
&self,
f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
) -> Option<T> {
self.borrow_overlay().as_ref().map(f)
}
fn with_overlay_mut_maybe<T>(
&mut self,
f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
) -> Option<T> {
self.with_overlay_mut(|overlay| overlay.as_mut().map(f))
}
}
impl<'a, 'b, Message, Renderer, Dependency, View>
overlay::Overlay<Message, Renderer>
for Overlay<'a, 'b, Message, Renderer, Dependency, View>
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,
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
) {
let _ = self.with_overlay_maybe(|overlay| {
overlay.draw(renderer, theme, 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 on_event(
&mut self,
event: iced_native::Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_overlay_mut_maybe(|overlay| {
overlay.on_event(
event,
layout,
cursor_position,
renderer,
clipboard,
shell,
)
})
.unwrap_or(iced_native::event::Status::Ignored)
}
}
impl<'a, Message, Renderer, Dependency, View>
From<Lazy<'a, Message, Renderer, Dependency, View>>
for Element<'a, Message, Renderer>
where
View: Into<Element<'static, Message, Renderer>> + 'static,
Renderer: iced_native::Renderer + 'static,
Message: 'static,
Dependency: Hash + 'a,
{
fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self {
Self::new(lazy)
}
}

View file

@ -17,15 +17,30 @@
clippy::type_complexity
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod lazy;
pub mod component;
pub mod responsive;
pub use component::Component;
pub use lazy::Lazy;
pub use responsive::Responsive;
mod cache;
use iced_native::{Element, Size};
use std::hash::Hash;
pub fn lazy<'a, Message, Renderer, Dependency, View>(
dependency: Dependency,
view: impl Fn() -> View + 'a,
) -> Lazy<'a, Message, Renderer, Dependency, View>
where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Renderer>>,
{
Lazy::new(dependency, view)
}
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.

View file

@ -92,7 +92,7 @@ where
is_secure: false,
font: Default::default(),
width: Length::Fill,
padding: Padding::ZERO,
padding: Padding::new(5),
size: None,
on_change: Box::new(on_change),
on_paste: None,