summaryrefslogtreecommitdiffstats
path: root/examples/todos/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'examples/todos/src/main.rs')
-rw-r--r--examples/todos/src/main.rs360
1 files changed, 181 insertions, 179 deletions
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 363e3fb8..bb00aac6 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -1,16 +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::event::{self, Event};
+use iced::keyboard;
+use iced::subscription;
use iced::theme::{self, Theme};
-use iced::{
- Application, Checkbox, Color, Column, Command, Container, Element, Font,
- Length, Row, Settings, Text,
+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)]
@@ -21,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,
}
@@ -39,12 +51,13 @@ enum Message {
CreateTask,
FilterChanged(Filter),
TaskMessage(usize, TaskMessage),
+ TabPressed { shift: bool },
}
impl Application for Todos {
- type Executor = iced::executor::Default;
- type Theme = Theme;
type Message = Message;
+ type Theme = Theme;
+ type Executor = iced::executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Todos, Command<Message>) {
@@ -81,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() {
@@ -97,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;
@@ -135,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)
.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...",
@@ -193,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)]
@@ -223,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
}
}
@@ -250,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,
}
}
@@ -266,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(theme::Button::Text),
- )
- .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(theme::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,
-}
-
-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 {
- theme::Button::Primary
- } else {
- theme::Button::Text
- });
+fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
+ let tasks_left = tasks.iter().filter(|task| !task.completed).count();
- button.on_press(Message::FilterChanged(filter)).padding(8)
- };
+ let filter_button = |label, filter, current_filter| {
+ let label = text(label).size(16);
- 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)]
@@ -436,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),
)
@@ -447,9 +449,9 @@ 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)
@@ -464,11 +466,11 @@ 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())
+ text(unicode.to_string())
.font(ICONS)
.width(Length::Units(20))
.horizontal_alignment(alignment::Horizontal::Center)