summaryrefslogtreecommitdiffstats
path: root/examples/pure
diff options
context:
space:
mode:
Diffstat (limited to 'examples/pure')
-rw-r--r--examples/pure/todos/Cargo.toml19
-rw-r--r--examples/pure/todos/README.md20
-rw-r--r--examples/pure/todos/index.html12
-rw-r--r--examples/pure/todos/src/main.rs599
4 files changed, 650 insertions, 0 deletions
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
+ }
+ }
+ }
+}