Implement pure version of todos example 🎉

The `Widget` trait in `iced_pure` needed to change a bit to make the
implementation of `Element::map` possible.

Specifically, the `children` method has been split into `diff` and
`children_state`.
This commit is contained in:
Héctor Ramón Jiménez 2022-02-12 17:21:28 +07:00
parent e3108494e5
commit bd22cc0bc0
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
18 changed files with 917 additions and 96 deletions

View file

@ -91,6 +91,7 @@ members = [
"examples/tour",
"examples/url_handler",
"examples/pure/counter",
"examples/pure/todos",
"examples/websocket",
]

View file

@ -0,0 +1,19 @@
[package]
name = "pure_todos"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../../..", features = ["async-std", "debug", "pure"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-std = "1.0"
directories-next = "2.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["Window", "Storage"] }
wasm-timer = "0.2"

View file

@ -0,0 +1,20 @@
## Todos
A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them.
All the example code is located in the __[`main`]__ file.
<div align="center">
<a href="https://gfycat.com/littlesanehalicore">
<img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="400px">
</a>
</div>
You can run the native version with `cargo run`:
```
cargo run --package todos
```
We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_!
[`main`]: src/main.rs
[TodoMVC]: http://todomvc.com/

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Todos - Iced</title>
<base data-trunk-public-url />
</head>
<body>
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="todos" />
</body>
</html>

View file

@ -0,0 +1,599 @@
use iced::alignment::{self, Alignment};
use iced::pure::widget::{
button, checkbox, column, container, row, scrollable, text, text_input,
};
use iced::pure::{Application, Element, Text};
use iced::{Command, Font, Length, Settings};
use serde::{Deserialize, Serialize};
pub fn main() -> iced::Result {
Todos::run(Settings::default())
}
#[derive(Debug)]
enum Todos {
Loading,
Loaded(State),
}
#[derive(Debug, Default)]
struct State {
input_value: String,
filter: Filter,
tasks: Vec<Task>,
dirty: bool,
saving: bool,
}
#[derive(Debug, Clone)]
enum Message {
Loaded(Result<SavedState, LoadError>),
Saved(Result<(), SaveError>),
InputChanged(String),
CreateTask,
FilterChanged(Filter),
TaskMessage(usize, TaskMessage),
}
impl Application for Todos {
type Executor = iced::executor::Default;
type Message = Message;
type Flags = ();
fn new(_flags: ()) -> (Todos, Command<Message>) {
(
Todos::Loading,
Command::perform(SavedState::load(), Message::Loaded),
)
}
fn title(&self) -> String {
let dirty = match self {
Todos::Loading => false,
Todos::Loaded(state) => state.dirty,
};
format!("Todos{} - Iced", if dirty { "*" } else { "" })
}
fn update(&mut self, message: Message) -> Command<Message> {
match self {
Todos::Loading => {
match message {
Message::Loaded(Ok(state)) => {
*self = Todos::Loaded(State {
input_value: state.input_value,
filter: state.filter,
tasks: state.tasks,
..State::default()
});
}
Message::Loaded(Err(_)) => {
*self = Todos::Loaded(State::default());
}
_ => {}
}
Command::none()
}
Todos::Loaded(state) => {
let mut saved = false;
match message {
Message::InputChanged(value) => {
state.input_value = value;
}
Message::CreateTask => {
if !state.input_value.is_empty() {
state
.tasks
.push(Task::new(state.input_value.clone()));
state.input_value.clear();
}
}
Message::FilterChanged(filter) => {
state.filter = filter;
}
Message::TaskMessage(i, TaskMessage::Delete) => {
state.tasks.remove(i);
}
Message::TaskMessage(i, task_message) => {
if let Some(task) = state.tasks.get_mut(i) {
task.update(task_message);
}
}
Message::Saved(_) => {
state.saving = false;
saved = true;
}
_ => {}
}
if !saved {
state.dirty = true;
}
if state.dirty && !state.saving {
state.dirty = false;
state.saving = true;
Command::perform(
SavedState {
input_value: state.input_value.clone(),
filter: state.filter,
tasks: state.tasks.clone(),
}
.save(),
Message::Saved,
)
} else {
Command::none()
}
}
}
}
fn view(&self) -> Element<Message> {
match self {
Todos::Loading => loading_message(),
Todos::Loaded(State {
input_value,
filter,
tasks,
..
}) => {
let title = text("todos")
.width(Length::Fill)
.size(100)
.color([0.5, 0.5, 0.5])
.horizontal_alignment(alignment::Horizontal::Center);
let input = text_input(
"What needs to be done?",
input_value,
Message::InputChanged,
)
.padding(15)
.size(30)
.on_submit(Message::CreateTask);
let controls = view_controls(&tasks, *filter);
let filtered_tasks =
tasks.iter().filter(|task| filter.matches(task));
let tasks: Element<_> = if filtered_tasks.count() > 0 {
tasks
.iter()
.enumerate()
.filter(|(_, task)| filter.matches(task))
.fold(column().spacing(20), |column, (i, task)| {
column.push(task.view().map(move |message| {
Message::TaskMessage(i, message)
}))
})
.into()
} else {
empty_message(match filter {
Filter::All => "You have not created a task yet...",
Filter::Active => "All your tasks are done! :D",
Filter::Completed => {
"You have not completed a task yet..."
}
})
};
let content = column()
.spacing(20)
.push(title)
.push(input)
.push(controls)
.push(tasks);
scrollable(
container(content)
.width(Length::Fill)
.padding(40)
.center_x(),
)
.into()
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Task {
description: String,
completed: bool,
#[serde(skip)]
state: TaskState,
}
#[derive(Debug, Clone)]
pub enum TaskState {
Idle,
Editing,
}
impl Default for TaskState {
fn default() -> Self {
Self::Idle
}
}
#[derive(Debug, Clone)]
pub enum TaskMessage {
Completed(bool),
Edit,
DescriptionEdited(String),
FinishEdition,
Delete,
}
impl Task {
fn new(description: String) -> Self {
Task {
description,
completed: false,
state: TaskState::Idle,
}
}
fn update(&mut self, message: TaskMessage) {
match message {
TaskMessage::Completed(completed) => {
self.completed = completed;
}
TaskMessage::Edit => {
self.state = TaskState::Editing;
}
TaskMessage::DescriptionEdited(new_description) => {
self.description = new_description;
}
TaskMessage::FinishEdition => {
if !self.description.is_empty() {
self.state = TaskState::Idle;
}
}
TaskMessage::Delete => {}
}
}
fn view(&self) -> Element<TaskMessage> {
match &self.state {
TaskState::Idle => {
let checkbox = checkbox(
&self.description,
self.completed,
TaskMessage::Completed,
)
.width(Length::Fill);
row()
.spacing(20)
.align_items(Alignment::Center)
.push(checkbox)
.push(
button(edit_icon())
.on_press(TaskMessage::Edit)
.padding(10)
.style(style::Button::Icon),
)
.into()
}
TaskState::Editing => {
let text_input = text_input(
"Describe your task...",
&self.description,
TaskMessage::DescriptionEdited,
)
.on_submit(TaskMessage::FinishEdition)
.padding(10);
row()
.spacing(20)
.align_items(Alignment::Center)
.push(text_input)
.push(
button(
row()
.spacing(10)
.push(delete_icon())
.push("Delete"),
)
.on_press(TaskMessage::Delete)
.padding(10)
.style(style::Button::Destructive),
)
.into()
}
}
}
}
fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
let tasks_left = tasks.iter().filter(|task| !task.completed).count();
let filter_button = |label, filter, current_filter| {
let label = text(label).size(16);
let button = button(label).style(if filter == current_filter {
style::Button::FilterSelected
} else {
style::Button::FilterActive
});
button.on_press(Message::FilterChanged(filter)).padding(8)
};
row()
.spacing(20)
.align_items(Alignment::Center)
.push(
text(format!(
"{} {} left",
tasks_left,
if tasks_left == 1 { "task" } else { "tasks" }
))
.width(Length::Fill)
.size(16),
)
.push(
row()
.width(Length::Shrink)
.spacing(10)
.push(filter_button("All", Filter::All, current_filter))
.push(filter_button("Active", Filter::Active, current_filter))
.push(filter_button(
"Completed",
Filter::Completed,
current_filter,
)),
)
.into()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Filter {
All,
Active,
Completed,
}
impl Default for Filter {
fn default() -> Self {
Filter::All
}
}
impl Filter {
fn matches(&self, task: &Task) -> bool {
match self {
Filter::All => true,
Filter::Active => !task.completed,
Filter::Completed => task.completed,
}
}
}
fn loading_message<'a>() -> Element<'a, Message> {
container(
text("Loading...")
.horizontal_alignment(alignment::Horizontal::Center)
.size(50),
)
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.into()
}
fn empty_message(message: &str) -> Element<'_, Message> {
container(
text(message)
.width(Length::Fill)
.size(25)
.horizontal_alignment(alignment::Horizontal::Center)
.color([0.7, 0.7, 0.7]),
)
.width(Length::Fill)
.height(Length::Units(200))
.center_y()
.into()
}
// Fonts
const ICONS: Font = Font::External {
name: "Icons",
bytes: include_bytes!("../../../todos/fonts/icons.ttf"),
};
fn icon(unicode: char) -> Text {
Text::new(unicode.to_string())
.font(ICONS)
.width(Length::Units(20))
.horizontal_alignment(alignment::Horizontal::Center)
.size(20)
}
fn edit_icon() -> Text {
icon('\u{F303}')
}
fn delete_icon() -> Text {
icon('\u{F1F8}')
}
// Persistence
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SavedState {
input_value: String,
filter: Filter,
tasks: Vec<Task>,
}
#[derive(Debug, Clone)]
enum LoadError {
FileError,
FormatError,
}
#[derive(Debug, Clone)]
enum SaveError {
FileError,
WriteError,
FormatError,
}
#[cfg(not(target_arch = "wasm32"))]
impl SavedState {
fn path() -> std::path::PathBuf {
let mut path = if let Some(project_dirs) =
directories_next::ProjectDirs::from("rs", "Iced", "Todos")
{
project_dirs.data_dir().into()
} else {
std::env::current_dir().unwrap_or(std::path::PathBuf::new())
};
path.push("todos.json");
path
}
async fn load() -> Result<SavedState, LoadError> {
use async_std::prelude::*;
let mut contents = String::new();
let mut file = async_std::fs::File::open(Self::path())
.await
.map_err(|_| LoadError::FileError)?;
file.read_to_string(&mut contents)
.await
.map_err(|_| LoadError::FileError)?;
serde_json::from_str(&contents).map_err(|_| LoadError::FormatError)
}
async fn save(self) -> Result<(), SaveError> {
use async_std::prelude::*;
let json = serde_json::to_string_pretty(&self)
.map_err(|_| SaveError::FormatError)?;
let path = Self::path();
if let Some(dir) = path.parent() {
async_std::fs::create_dir_all(dir)
.await
.map_err(|_| SaveError::FileError)?;
}
{
let mut file = async_std::fs::File::create(path)
.await
.map_err(|_| SaveError::FileError)?;
file.write_all(json.as_bytes())
.await
.map_err(|_| SaveError::WriteError)?;
}
// This is a simple way to save at most once every couple seconds
async_std::task::sleep(std::time::Duration::from_secs(2)).await;
Ok(())
}
}
#[cfg(target_arch = "wasm32")]
impl SavedState {
fn storage() -> Option<web_sys::Storage> {
let window = web_sys::window()?;
window.local_storage().ok()?
}
async fn load() -> Result<SavedState, LoadError> {
let storage = Self::storage().ok_or(LoadError::FileError)?;
let contents = storage
.get_item("state")
.map_err(|_| LoadError::FileError)?
.ok_or(LoadError::FileError)?;
serde_json::from_str(&contents).map_err(|_| LoadError::FormatError)
}
async fn save(self) -> Result<(), SaveError> {
let storage = Self::storage().ok_or(SaveError::FileError)?;
let json = serde_json::to_string_pretty(&self)
.map_err(|_| SaveError::FormatError)?;
storage
.set_item("state", &json)
.map_err(|_| SaveError::WriteError)?;
let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await;
Ok(())
}
}
mod style {
use iced::{button, Background, Color, Vector};
pub enum Button {
FilterActive,
FilterSelected,
Icon,
Destructive,
}
impl button::StyleSheet for Button {
fn active(&self) -> button::Style {
match self {
Button::FilterActive => button::Style::default(),
Button::FilterSelected => button::Style {
background: Some(Background::Color(Color::from_rgb(
0.2, 0.2, 0.7,
))),
border_radius: 10.0,
text_color: Color::WHITE,
..button::Style::default()
},
Button::Icon => button::Style {
text_color: Color::from_rgb(0.5, 0.5, 0.5),
..button::Style::default()
},
Button::Destructive => button::Style {
background: Some(Background::Color(Color::from_rgb(
0.8, 0.2, 0.2,
))),
border_radius: 5.0,
text_color: Color::WHITE,
shadow_offset: Vector::new(1.0, 1.0),
..button::Style::default()
},
}
}
fn hovered(&self) -> button::Style {
let active = self.active();
button::Style {
text_color: match self {
Button::Icon => Color::from_rgb(0.2, 0.2, 0.7),
Button::FilterActive => Color::from_rgb(0.2, 0.2, 0.7),
_ => active.text_color,
},
shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0),
..active
}
}
}
}

View file

@ -53,7 +53,8 @@ impl State {
impl<'a, Message, Renderer> iced_native::Widget<Message, Renderer>
for Pure<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
Message: 'a,
Renderer: iced_native::Renderer + 'a,
{
fn width(&self) -> Length {
self.element.as_widget().width()

View file

@ -33,7 +33,9 @@ pub trait Widget<Message, Renderer> {
fn state(&self) -> Box<dyn Any>;
fn children(&self) -> &[Element<Message, Renderer>];
fn diff(&self, tree: &mut Tree);
fn children_state(&self) -> Vec<Tree>;
fn width(&self) -> Length;
@ -99,11 +101,13 @@ pub fn row<'a, Message, Renderer>() -> Row<'a, Message, Renderer> {
Row::new()
}
pub fn scrollable<'a, Message, Renderer>() -> Scrollable<'a, Message, Renderer>
pub fn scrollable<'a, Message, Renderer>(
content: impl Into<Element<'a, Message, Renderer>>,
) -> Scrollable<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
{
Scrollable::new()
Scrollable::new(content)
}
pub fn button<'a, Message, Renderer>(

View file

@ -85,8 +85,12 @@ where
Box::new(State::new())
}
fn children(&self) -> &[Element<Message, Renderer>] {
std::slice::from_ref(&self.content)
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
fn children_state(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn width(&self) -> Length {

View file

@ -22,8 +22,10 @@ where
Box::new(())
}
fn children(&self) -> &[Element<Message, Renderer>] {
&[]
fn diff(&self, _tree: &mut Tree) {}
fn children_state(&self) -> Vec<Tree> {
Vec::new()
}
fn width(&self) -> Length {

View file

@ -85,8 +85,12 @@ where
Box::new(())
}
fn children(&self) -> &[Element<Message, Renderer>] {
&self.children
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&self.children);
}
fn children_state(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn width(&self) -> Length {

View file

@ -132,8 +132,12 @@ where
Box::new(())
}
fn children(&self) -> &[Element<Message, Renderer>] {
std::slice::from_ref(&self.content)
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
fn children_state(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn width(&self) -> Length {

View file

@ -1,4 +1,12 @@
use crate::Widget;
use crate::widget::{Tree, Widget};
use iced_native::event::{self, Event};
use iced_native::layout::{self, Layout};
use iced_native::mouse;
use iced_native::renderer;
use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell};
use std::any::{self, Any};
pub struct Element<'a, Message, Renderer> {
widget: Box<dyn Widget<Message, Renderer> + 'a>,
@ -18,4 +26,143 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> {
self.widget.as_mut()
}
pub fn map<B>(
self,
f: impl Fn(Message) -> B + 'a,
) -> Element<'a, B, Renderer>
where
Message: 'a,
Renderer: iced_native::Renderer + 'a,
B: 'a,
{
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 + 'a>,
}
impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
pub fn new<F>(
widget: Box<dyn Widget<A, Renderer> + 'a>,
mapper: F,
) -> Map<'a, A, B, Renderer>
where
F: 'a + Fn(A) -> B,
{
Map {
widget,
mapper: Box::new(mapper),
}
}
}
impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer>
where
Renderer: iced_native::Renderer + 'a,
A: 'a,
B: 'a,
{
fn tag(&self) -> any::TypeId {
self.widget.tag()
}
fn state(&self) -> Box<dyn Any> {
self.widget.state()
}
fn diff(&self, tree: &mut Tree) {
self.widget.diff(tree)
}
fn children_state(&self) -> Vec<Tree> {
self.widget.children_state()
}
fn width(&self) -> Length {
self.widget.width()
}
fn height(&self) -> Length {
self.widget.height()
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.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<'_, B>,
) -> event::Status {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
let status = self.widget.on_event(
tree,
event,
layout,
cursor_position,
renderer,
clipboard,
&mut local_shell,
);
shell.merge(local_shell, &self.mapper);
status
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
viewport: &Rectangle,
) {
self.widget.draw(
tree,
renderer,
style,
layout,
cursor_position,
viewport,
)
}
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,
renderer,
)
}
fn hash_layout(&self, state: &mut Hasher) {
self.widget.hash_layout(state);
}
}

View file

@ -85,8 +85,12 @@ where
Box::new(())
}
fn children(&self) -> &[Element<Message, Renderer>] {
&self.children
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&self.children)
}
fn children_state(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn width(&self) -> Length {

View file

@ -1,4 +1,4 @@
use crate::widget::{Column, Tree};
use crate::widget::Tree;
use crate::{Element, Widget};
use iced_native::event::{self, Event};
@ -6,9 +6,7 @@ use iced_native::layout::{self, Layout};
use iced_native::mouse;
use iced_native::renderer;
use iced_native::widget::scrollable;
use iced_native::{
Alignment, Clipboard, Hasher, Length, Padding, Point, Rectangle, Shell,
};
use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell};
pub use iced_style::scrollable::StyleSheet;
@ -22,61 +20,33 @@ pub struct Scrollable<'a, Message, Renderer> {
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
content: Column<'a, Message, Renderer>,
on_scroll: Option<Box<dyn Fn(f32) -> Message>>,
style_sheet: Box<dyn StyleSheet + 'a>,
content: Element<'a, Message, Renderer>,
}
impl<'a, Message, Renderer: iced_native::Renderer>
Scrollable<'a, Message, Renderer>
{
/// Creates a new [`Scrollable`] with the given [`State`].
pub fn new() -> Self {
/// Creates a new [`Scrollable`].
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
Scrollable {
height: Length::Shrink,
scrollbar_width: 10,
scrollbar_margin: 0,
scroller_width: 10,
content: Column::new(),
on_scroll: None,
style_sheet: Default::default(),
content: content.into(),
}
}
/// 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);
self
}
/// Sets the height of the [`Scrollable`].
pub fn height(mut self, height: Length) -> Self {
self.height = 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 {
@ -115,15 +85,6 @@ impl<'a, Message, Renderer: iced_native::Renderer>
self.style_sheet = style_sheet.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>
@ -139,12 +100,16 @@ where
Box::new(scrollable::State::new())
}
fn children(&self) -> &[Element<Message, Renderer>] {
self.content.children()
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
fn children_state(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn width(&self) -> Length {
Widget::<Message, Renderer>::width(&self.content)
self.content.as_widget().width()
}
fn height(&self) -> Length {
@ -156,7 +121,7 @@ where
self.tag().hash(state);
self.height.hash(state);
self.content.hash_layout(state)
self.content.as_widget().hash_layout(state)
}
fn layout(
@ -169,7 +134,9 @@ where
limits,
Widget::<Message, Renderer>::width(self),
self.height,
|renderer, limits| self.content.layout(renderer, limits),
|renderer, limits| {
self.content.as_widget().layout(renderer, limits)
},
)
}
@ -195,7 +162,7 @@ where
self.scroller_width,
&self.on_scroll,
|event, layout, cursor_position, clipboard, shell| {
self.content.on_event(
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout,
@ -227,7 +194,7 @@ where
self.scroller_width,
self.style_sheet.as_ref(),
|renderer, layout, cursor_position, viewport| {
self.content.draw(
self.content.as_widget().draw(
&tree.children[0],
renderer,
style,
@ -255,7 +222,7 @@ where
self.scrollbar_margin,
self.scroller_width,
|layout, cursor_position, viewport| {
self.content.mouse_interaction(
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor_position,
@ -266,3 +233,16 @@ where
)
}
}
impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + iced_native::Renderer,
{
fn from(
text_input: Scrollable<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(text_input)
}
}

View file

@ -21,8 +21,10 @@ where
Box::new(())
}
fn children(&self) -> &[Element<Message, Renderer>] {
&[]
fn diff(&self, _tree: &mut Tree) {}
fn children_state(&self) -> Vec<Tree> {
Vec::new()
}
fn width(&self) -> Length {

View file

@ -146,8 +146,10 @@ where
Box::new(text_input::State::new())
}
fn children(&self) -> &[Element<Message, Renderer>] {
&[]
fn diff(&self, _tree: &mut Tree) {}
fn children_state(&self) -> Vec<Tree> {
Vec::new()
}
fn width(&self) -> Length {
@ -237,3 +239,16 @@ where
text_input::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,
{
fn from(
text_input: TextInput<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(text_input)
}
}

View file

@ -23,12 +23,7 @@ impl Tree {
Self {
tag: element.as_widget().tag(),
state: State(element.as_widget().state()),
children: element
.as_widget()
.children()
.iter()
.map(Self::new)
.collect(),
children: element.as_widget().children_state(),
}
}
@ -37,27 +32,32 @@ impl Tree {
new: &Element<'_, Message, Renderer>,
) {
if self.tag == new.as_widget().tag() {
let new_children = new.as_widget().children();
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())
{
child_state.diff(new);
}
if self.children.len() < new_children.len() {
self.children.extend(
new_children[self.children.len()..].iter().map(Self::new),
);
}
new.as_widget().diff(self)
} else {
*self = Self::new(new);
}
}
pub fn diff_children<Message, Renderer>(
&mut self,
new_children: &[Element<'_, Message, Renderer>],
) {
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())
{
child_state.diff(new);
}
if self.children.len() < new_children.len() {
self.children.extend(
new_children[self.children.len()..].iter().map(Self::new),
);
}
}
}
pub struct State(Box<dyn Any>);

View file

@ -17,12 +17,15 @@
//! [the original widgets]: crate::widget
//! [`button::State`]: crate::widget::button::State
//! [impure `Application`]: crate::Application
pub use iced_pure::{Element as _, *};
pub use iced_pure::{Element as _, Text as _, *};
/// A generic, pure [`Widget`].
pub type Element<'a, Message> =
iced_pure::Element<'a, Message, crate::Renderer>;
/// A pure text widget.
pub type Text = iced_pure::Text<crate::Renderer>;
mod application;
mod sandbox;