diff options
Diffstat (limited to '')
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | examples/pure/todos/Cargo.toml | 19 | ||||
-rw-r--r-- | examples/pure/todos/README.md | 20 | ||||
-rw-r--r-- | examples/pure/todos/index.html | 12 | ||||
-rw-r--r-- | examples/pure/todos/src/main.rs | 599 | ||||
-rw-r--r-- | pure/src/lib.rs | 3 | ||||
-rw-r--r-- | pure/src/widget.rs | 10 | ||||
-rw-r--r-- | pure/src/widget/button.rs | 8 | ||||
-rw-r--r-- | pure/src/widget/checkbox.rs | 6 | ||||
-rw-r--r-- | pure/src/widget/column.rs | 8 | ||||
-rw-r--r-- | pure/src/widget/container.rs | 8 | ||||
-rw-r--r-- | pure/src/widget/element.rs | 149 | ||||
-rw-r--r-- | pure/src/widget/row.rs | 8 | ||||
-rw-r--r-- | pure/src/widget/scrollable.rs | 86 | ||||
-rw-r--r-- | pure/src/widget/text.rs | 6 | ||||
-rw-r--r-- | pure/src/widget/text_input.rs | 19 | ||||
-rw-r--r-- | pure/src/widget/tree.rs | 44 | ||||
-rw-r--r-- | src/pure.rs | 5 |
18 files changed, 916 insertions, 95 deletions
@@ -91,6 +91,7 @@ members = [ "examples/tour", "examples/url_handler", "examples/pure/counter", + "examples/pure/todos", "examples/websocket", ] diff --git a/examples/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml new file mode 100644 index 00000000..fa14ff6a --- /dev/null +++ b/examples/pure/todos/Cargo.toml @@ -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" diff --git a/examples/pure/todos/README.md b/examples/pure/todos/README.md new file mode 100644 index 00000000..9c2598b9 --- /dev/null +++ b/examples/pure/todos/README.md @@ -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/ diff --git a/examples/pure/todos/index.html b/examples/pure/todos/index.html new file mode 100644 index 00000000..ee5570fb --- /dev/null +++ b/examples/pure/todos/index.html @@ -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> diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs new file mode 100644 index 00000000..9acc9fcc --- /dev/null +++ b/examples/pure/todos/src/main.rs @@ -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 + } + } + } +} diff --git a/pure/src/lib.rs b/pure/src/lib.rs index a179a84b..07f068cc 100644 --- a/pure/src/lib.rs +++ b/pure/src/lib.rs @@ -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() diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 8df64426..302a057a 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -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>( diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index d0f9e53e..6dc1016c 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -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 { diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs index 8ee2b7bb..1cfe89a8 100644 --- a/pure/src/widget/checkbox.rs +++ b/pure/src/widget/checkbox.rs @@ -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 { diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index ed097d33..68d3c4b4 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -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 { diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index 94a6b07b..85ea8039 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -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 { diff --git a/pure/src/widget/element.rs b/pure/src/widget/element.rs index aedf5973..2a137d40 100644 --- a/pure/src/widget/element.rs +++ b/pure/src/widget/element.rs @@ -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); + } } diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 147a0850..ec7e144c 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -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 { diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index 69acabb7..badc9fc2 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -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) + } +} diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index f437b48b..8f157ea0 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -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 { diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs index 06ab2910..e18a2bf0 100644 --- a/pure/src/widget/text_input.rs +++ b/pure/src/widget/text_input.rs @@ -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) + } +} diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 98e976ad..3a5f4433 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -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,25 +32,30 @@ impl Tree { new: &Element<'_, Message, Renderer>, ) { if self.tag == new.as_widget().tag() { - let new_children = new.as_widget().children(); + new.as_widget().diff(self) + } else { + *self = Self::new(new); + } + } - if self.children.len() > new_children.len() { - self.children.truncate(new_children.len()); - } + 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); - } + 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), - ); - } - } else { - *self = Self::new(new); + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(Self::new), + ); } } } diff --git a/src/pure.rs b/src/pure.rs index 7fe2bc59..29495c07 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -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; |