diff options
Diffstat (limited to '')
| -rw-r--r-- | examples/todos.rs | 340 | 
1 files changed, 254 insertions, 86 deletions
| diff --git a/examples/todos.rs b/examples/todos.rs index d97a9e08..2cdf1ea1 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -3,23 +3,34 @@ use iced::{      Application, Background, Button, Checkbox, Color, Column, Command,      Container, Element, Font, Length, Row, Scrollable, Text, TextInput,  }; +use serde::{Deserialize, Serialize};  pub fn main() {      Todos::run()  } +#[derive(Debug)] +enum Todos { +    Loading, +    Loaded(State), +} +  #[derive(Debug, Default)] -struct Todos { +struct State {      scroll: scrollable::State,      input: text_input::State,      input_value: String,      filter: Filter,      tasks: Vec<Task>,      controls: Controls, +    dirty: bool, +    saving: bool,  }  #[derive(Debug, Clone)] -pub enum Message { +enum Message { +    Loaded(Result<SavedState, LoadError>), +    Saved(Result<(), SaveError>),      InputChanged(String),      CreateTask,      FilterChanged(Filter), @@ -30,113 +41,175 @@ impl Application for Todos {      type Message = Message;      fn new() -> (Todos, Command<Message>) { -        (Todos::default(), Command::none()) +        (Todos::Loading, Command::attempt(load(), Message::Loaded))      }      fn title(&self) -> String { -        String::from("Todos - Iced") +        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 message { -            Message::InputChanged(value) => { -                self.input_value = value; -            } -            Message::CreateTask => { -                if !self.input_value.is_empty() { -                    self.tasks.push(Task::new(self.input_value.clone())); -                    self.input_value.clear(); +        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()              } -            Message::FilterChanged(filter) => { -                self.filter = filter; -            } -            Message::TaskMessage(i, TaskMessage::Delete) => { -                self.tasks.remove(i); -            } -            Message::TaskMessage(i, task_message) => { -                if let Some(task) = self.tasks.get_mut(i) { -                    task.update(task_message); +            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; +                    } +                    _ => {}                  } -            } -        } -        dbg!(self); +                if !saved { +                    state.dirty = true; +                } -        Command::none() +                if state.dirty && !state.saving { +                    state.dirty = false; +                    state.saving = true; + +                    Command::attempt( +                        save(SavedState { +                            input_value: state.input_value.clone(), +                            filter: state.filter, +                            tasks: state.tasks.clone(), +                        }), +                        Message::Saved, +                    ) +                } else { +                    Command::none() +                } +            } +        }      }      fn view(&mut self) -> Element<Message> { -        let Todos { -            scroll, -            input, -            input_value, -            filter, -            tasks, -            controls, -        } = self; - -        let title = Text::new("todos") -            .size(100) -            .color([0.5, 0.5, 0.5]) -            .horizontal_alignment(HorizontalAlignment::Center); - -        let input = TextInput::new( -            input, -            "What needs to be done?", -            input_value, -            Message::InputChanged, -        ) -        .padding(15) -        .size(30) -        .on_submit(Message::CreateTask); - -        let controls = controls.view(&tasks, *filter); -        let filtered_tasks = tasks.iter().filter(|task| filter.matches(task)); - -        let tasks: Element<_> = -            if filtered_tasks.count() > 0 { -                tasks -                    .iter_mut() -                    .enumerate() -                    .filter(|(_, task)| filter.matches(task)) -                    .fold(Column::new().spacing(20), |column, (i, task)| { -                        column.push(task.view().map(move |message| { -                            Message::TaskMessage(i, message) -                        })) +        match self { +            Todos::Loading => loading_message(), +            Todos::Loaded(State { +                scroll, +                input, +                input_value, +                filter, +                tasks, +                controls, +                .. +            }) => { +                let title = Text::new("todos") +                    .size(100) +                    .color([0.5, 0.5, 0.5]) +                    .horizontal_alignment(HorizontalAlignment::Center); + +                let input = TextInput::new( +                    input, +                    "What needs to be done?", +                    input_value, +                    Message::InputChanged, +                ) +                .padding(15) +                .size(30) +                .on_submit(Message::CreateTask); + +                let controls = controls.view(&tasks, *filter); +                let filtered_tasks = +                    tasks.iter().filter(|task| filter.matches(task)); + +                let tasks: Element<_> = if filtered_tasks.count() > 0 { +                    tasks +                        .iter_mut() +                        .enumerate() +                        .filter(|(_, task)| filter.matches(task)) +                        .fold(Column::new().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..." +                        }                      }) -                    .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::new() -            .max_width(800) -            .spacing(20) -            .push(title) -            .push(input) -            .push(controls) -            .push(tasks); - -        Scrollable::new(scroll) -            .padding(40) -            .push(Container::new(content).width(Length::Fill).center_x()) -            .into() +                let content = Column::new() +                    .max_width(800) +                    .spacing(20) +                    .push(title) +                    .push(input) +                    .push(controls) +                    .push(tasks); + +                Scrollable::new(scroll) +                    .padding(40) +                    .push( +                        Container::new(content).width(Length::Fill).center_x(), +                    ) +                    .into() +            } +        }      }  } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)]  struct Task {      description: String,      completed: bool, + +    #[serde(skip)]      state: TaskState,  } -#[derive(Debug)] +#[derive(Debug, Clone)]  pub enum TaskState {      Idle {          edit_button: button::State, @@ -147,6 +220,14 @@ pub enum TaskState {      },  } +impl Default for TaskState { +    fn default() -> Self { +        TaskState::Idle { +            edit_button: button::State::new(), +        } +    } +} +  #[derive(Debug, Clone)]  pub enum TaskMessage {      Completed(bool), @@ -255,7 +336,7 @@ impl Task {      }  } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)]  pub struct Controls {      all_button: button::State,      active_button: button::State, @@ -324,7 +405,7 @@ impl Controls {      }  } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]  pub enum Filter {      All,      Active, @@ -347,6 +428,18 @@ impl Filter {      }  } +fn loading_message() -> Element<'static, Message> { +    Container::new( +        Text::new("Loading...") +            .horizontal_alignment(HorizontalAlignment::Center) +            .size(50), +    ) +    .width(Length::Fill) +    .height(Length::Fill) +    .center_y() +    .into() +} +  fn empty_message(message: &str) -> Element<'static, Message> {      Container::new(          Text::new(message) @@ -381,3 +474,78 @@ fn edit_icon() -> Text {  fn delete_icon() -> Text {      icon('\u{F1F8}')  } + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { +    input_value: String, +    filter: Filter, +    tasks: Vec<Task>, +} + +fn save_path() -> std::path::PathBuf { +    let mut path = if let Some(project_dirs) = +        directories::ProjectDirs::from("rs", "Iced", "Todos") +    { +        project_dirs.data_dir().into() +    } else { +        std::env::current_dir() +            .expect("The current directory is not accessible") +    }; + +    path.push("todos.json"); + +    path +} + +#[derive(Debug, Clone)] +enum LoadError { +    FileError, +    FormatError, +} + +async fn load() -> Result<SavedState, LoadError> { +    use std::io::Read; + +    let mut contents = String::new(); + +    let mut file = +        std::fs::File::open(save_path()).map_err(|_| LoadError::FileError)?; + +    file.read_to_string(&mut contents) +        .map_err(|_| LoadError::FileError)?; + +    serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) +} + +#[derive(Debug, Clone)] +enum SaveError { +    DirectoryError, +    FileError, +    WriteError, +    FormatError, +} + +async fn save(state: SavedState) -> Result<(), SaveError> { +    use std::io::Write; + +    let json = serde_json::to_string_pretty(&state) +        .map_err(|_| SaveError::FormatError)?; + +    let save_path = save_path(); +    let save_dir = save_path.parent().ok_or(SaveError::DirectoryError)?; + +    std::fs::create_dir_all(save_dir).map_err(|_| SaveError::DirectoryError)?; + +    let mut file = +        std::fs::File::create(save_path).map_err(|_| SaveError::FileError)?; + +    file.write_all(json.as_bytes()) +        .map_err(|_| SaveError::WriteError)?; + +    // This is a simple way to save at most once every couple seconds +    // We will be able to get rid of it once we implement event subscriptions +    std::thread::sleep(std::time::Duration::from_secs(2)); + +    Ok(()) +} | 
