diff options
Diffstat (limited to 'examples/todos/src')
-rw-r--r-- | examples/todos/src/main.rs | 464 |
1 files changed, 207 insertions, 257 deletions
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 0b889407..bddc0e71 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,15 +1,31 @@ use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; -use iced::scrollable::{self, Scrollable}; -use iced::text_input::{self, TextInput}; -use iced::{ - Application, Checkbox, Column, Command, Container, Element, Font, Length, - Row, Settings, Text, +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; +use iced::theme::{self, Theme}; +use iced::widget::{ + self, button, checkbox, column, container, row, scrollable, text, + text_input, Text, }; +use iced::window; +use iced::{Application, Element}; +use iced::{Color, Command, Font, Length, Settings, Subscription}; + +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +lazy_static! { + static ref INPUT_ID: text_input::Id = text_input::Id::unique(); +} + pub fn main() -> iced::Result { - Todos::run(Settings::default()) + Todos::run(Settings { + window: window::Settings { + size: (500, 800), + ..window::Settings::default() + }, + ..Settings::default() + }) } #[derive(Debug)] @@ -20,12 +36,9 @@ enum Todos { #[derive(Debug, Default)] struct State { - scroll: scrollable::State, - input: text_input::State, input_value: String, filter: Filter, tasks: Vec<Task>, - controls: Controls, dirty: bool, saving: bool, } @@ -38,11 +51,13 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, } impl Application for Todos { - type Executor = iced::executor::Default; type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; type Flags = (); fn new(_flags: ()) -> (Todos, Command<Message>) { @@ -79,14 +94,16 @@ impl Application for Todos { _ => {} } - Command::none() + text_input::focus(INPUT_ID.clone()) } Todos::Loaded(state) => { let mut saved = false; - match message { + let command = match message { Message::InputChanged(value) => { state.input_value = value; + + Command::none() } Message::CreateTask => { if !state.input_value.is_empty() { @@ -95,30 +112,56 @@ impl Application for Todos { .push(Task::new(state.input_value.clone())); state.input_value.clear(); } + + Command::none() } Message::FilterChanged(filter) => { state.filter = filter; + + Command::none() } Message::TaskMessage(i, TaskMessage::Delete) => { state.tasks.remove(i); + + Command::none() } Message::TaskMessage(i, task_message) => { if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + task.update(task_message); + + if should_focus { + text_input::focus(Task::text_input_id(i)) + } else { + Command::none() + } + } else { + Command::none() } } Message::Saved(_) => { state.saving = false; saved = true; + + Command::none() } - _ => {} - } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + _ => Command::none(), + }; if !saved { state.dirty = true; } - if state.dirty && !state.saving { + let save = if state.dirty && !state.saving { state.dirty = false; state.saving = true; @@ -133,54 +176,57 @@ impl Application for Todos { ) } else { Command::none() - } + }; + + Command::batch(vec![command, save]) } } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { match self { Todos::Loading => loading_message(), Todos::Loaded(State { - scroll, - input, input_value, filter, tasks, - controls, .. }) => { - let title = Text::new("todos") + let title = text("todos") .width(Length::Fill) .size(100) - .color([0.5, 0.5, 0.5]) + .style(Color::from([0.5, 0.5, 0.5])) .horizontal_alignment(alignment::Horizontal::Center); - let input = TextInput::new( - input, + let input = text_input( "What needs to be done?", input_value, Message::InputChanged, ) + .id(INPUT_ID.clone()) .padding(15) .size(30) .on_submit(Message::CreateTask); - let controls = controls.view(&tasks, *filter); + 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_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() + column( + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .map(|(i, task)| { + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }) + }) + .collect(), + ) + .spacing(10) + .into() } else { empty_message(match filter { Filter::All => "You have not created a task yet...", @@ -191,23 +237,36 @@ impl Application for Todos { }) }; - let content = Column::new() - .max_width(800) + let content = column![title, input, controls, tasks] .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() + .max_width(800); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(), + ) + .into() } } } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + _ => None, + }) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -221,20 +280,13 @@ struct Task { #[derive(Debug, Clone)] pub enum TaskState { - Idle { - edit_button: button::State, - }, - Editing { - text_input: text_input::State, - delete_button: button::State, - }, + Idle, + Editing, } impl Default for TaskState { fn default() -> Self { - TaskState::Idle { - edit_button: button::State::new(), - } + Self::Idle } } @@ -248,13 +300,15 @@ pub enum TaskMessage { } impl Task { + fn text_input_id(i: usize) -> text_input::Id { + text_input::Id::new(format!("task-{}", i)) + } + fn new(description: String) -> Self { Task { description, completed: false, - state: TaskState::Idle { - edit_button: button::State::new(), - }, + state: TaskState::Idle, } } @@ -264,150 +318,100 @@ impl Task { self.completed = completed; } TaskMessage::Edit => { - let mut text_input = text_input::State::focused(); - text_input.select_all(); - - self.state = TaskState::Editing { - text_input, - delete_button: button::State::new(), - }; + self.state = TaskState::Editing; } TaskMessage::DescriptionEdited(new_description) => { self.description = new_description; } TaskMessage::FinishEdition => { if !self.description.is_empty() { - self.state = TaskState::Idle { - edit_button: button::State::new(), - } + self.state = TaskState::Idle; } } TaskMessage::Delete => {} } } - fn view(&mut self) -> Element<TaskMessage> { - match &mut self.state { - TaskState::Idle { edit_button } => { - let checkbox = Checkbox::new( - self.completed, + fn view(&self, i: usize) -> Element<TaskMessage> { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox( &self.description, + self.completed, TaskMessage::Completed, ) .width(Length::Fill); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(checkbox) - .push( - Button::new(edit_button, edit_icon()) - .on_press(TaskMessage::Edit) - .padding(10) - .style(style::Button::Icon), - ) - .into() + row![ + checkbox, + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(theme::Button::Text), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } - TaskState::Editing { - text_input, - delete_button, - } => { - let text_input = TextInput::new( - text_input, + TaskState::Editing => { + let text_input = text_input( "Describe your task...", &self.description, TaskMessage::DescriptionEdited, ) + .id(Self::text_input_id(i)) .on_submit(TaskMessage::FinishEdition) .padding(10); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(text_input) - .push( - Button::new( - delete_button, - Row::new() - .spacing(10) - .push(delete_icon()) - .push(Text::new("Delete")), - ) + row![ + text_input, + button(row![delete_icon(), "Delete"].spacing(10)) .on_press(TaskMessage::Delete) .padding(10) - .style(style::Button::Destructive), - ) - .into() + .style(theme::Button::Destructive) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } } } } -#[derive(Debug, Default, Clone)] -pub struct Controls { - all_button: button::State, - active_button: button::State, - completed_button: button::State, -} +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); -impl Controls { - fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row<Message> { - let Controls { - all_button, - active_button, - completed_button, - } = self; - - let tasks_left = tasks.iter().filter(|task| !task.completed).count(); - - let filter_button = |state, label, filter, current_filter| { - let label = Text::new(label).size(16); - let button = - Button::new(state, label).style(if filter == current_filter { - style::Button::FilterSelected - } else { - style::Button::FilterActive - }); + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); - button.on_press(Message::FilterChanged(filter)).padding(8) - }; - - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push( - Text::new(format!( - "{} {} left", - tasks_left, - if tasks_left == 1 { "task" } else { "tasks" } - )) - .width(Length::Fill) - .size(16), - ) - .push( - Row::new() - .width(Length::Shrink) - .spacing(10) - .push(filter_button( - all_button, - "All", - Filter::All, - current_filter, - )) - .push(filter_button( - active_button, - "Active", - Filter::Active, - current_filter, - )) - .push(filter_button( - completed_button, - "Completed", - Filter::Completed, - current_filter, - )), - ) - } + let button = button(label).style(if filter == current_filter { + theme::Button::Primary + } else { + theme::Button::Text + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row![ + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + row![ + filter_button("All", Filter::All, current_filter), + filter_button("Active", Filter::Active, current_filter), + filter_button("Completed", Filter::Completed, current_filter,), + ] + .width(Length::Shrink) + .spacing(10) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -434,8 +438,8 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - Container::new( - Text::new("Loading...") + container( + text("Loading...") .horizontal_alignment(alignment::Horizontal::Center) .size(50), ) @@ -445,13 +449,13 @@ fn loading_message<'a>() -> Element<'a, Message> { .into() } -fn empty_message<'a>(message: &str) -> Element<'a, Message> { - Container::new( - Text::new(message) +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]), + .style(Color::from([0.7, 0.7, 0.7])), ) .width(Length::Fill) .height(Length::Units(200)) @@ -462,22 +466,22 @@ fn empty_message<'a>(message: &str) -> Element<'a, Message> { // Fonts const ICONS: Font = Font::External { name: "Icons", - bytes: include_bytes!("../fonts/icons.ttf"), + bytes: include_bytes!("../../todos/fonts/icons.ttf"), }; -fn icon(unicode: char) -> Text { - Text::new(unicode.to_string()) +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) .font(ICONS) .width(Length::Units(20)) .horizontal_alignment(alignment::Horizontal::Center) .size(20) } -fn edit_icon() -> Text { +fn edit_icon() -> Text<'static> { icon('\u{F303}') } -fn delete_icon() -> Text { +fn delete_icon() -> Text<'static> { icon('\u{F1F8}') } @@ -491,15 +495,15 @@ struct SavedState { #[derive(Debug, Clone)] enum LoadError { - FileError, - FormatError, + File, + Format, } #[derive(Debug, Clone)] enum SaveError { - FileError, - WriteError, - FormatError, + File, + Write, + Format, } #[cfg(not(target_arch = "wasm32"))] @@ -510,7 +514,7 @@ impl SavedState { { project_dirs.data_dir().into() } else { - std::env::current_dir().unwrap_or(std::path::PathBuf::new()) + std::env::current_dir().unwrap_or_default() }; path.push("todos.json"); @@ -525,37 +529,37 @@ impl SavedState { let mut file = async_std::fs::File::open(Self::path()) .await - .map_err(|_| LoadError::FileError)?; + .map_err(|_| LoadError::File)?; file.read_to_string(&mut contents) .await - .map_err(|_| LoadError::FileError)?; + .map_err(|_| LoadError::File)?; - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + serde_json::from_str(&contents).map_err(|_| LoadError::Format) } async fn save(self) -> Result<(), SaveError> { use async_std::prelude::*; let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; + .map_err(|_| SaveError::Format)?; let path = Self::path(); if let Some(dir) = path.parent() { async_std::fs::create_dir_all(dir) .await - .map_err(|_| SaveError::FileError)?; + .map_err(|_| SaveError::File)?; } { let mut file = async_std::fs::File::create(path) .await - .map_err(|_| SaveError::FileError)?; + .map_err(|_| SaveError::File)?; file.write_all(json.as_bytes()) .await - .map_err(|_| SaveError::WriteError)?; + .map_err(|_| SaveError::Write)?; } // This is a simple way to save at most once every couple seconds @@ -574,82 +578,28 @@ impl SavedState { } async fn load() -> Result<SavedState, LoadError> { - let storage = Self::storage().ok_or(LoadError::FileError)?; + let storage = Self::storage().ok_or(LoadError::File)?; let contents = storage .get_item("state") - .map_err(|_| LoadError::FileError)? - .ok_or(LoadError::FileError)?; + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + serde_json::from_str(&contents).map_err(|_| LoadError::Format) } async fn save(self) -> Result<(), SaveError> { - let storage = Self::storage().ok_or(SaveError::FileError)?; + let storage = Self::storage().ok_or(SaveError::File)?; let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; + .map_err(|_| SaveError::Format)?; storage .set_item("state", &json) - .map_err(|_| SaveError::WriteError)?; + .map_err(|_| SaveError::Write)?; 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 - } - } - } -} |