Merge pull request #56 from hecrj/example/filter-todos
Draw checkmark icon and filter todos
This commit is contained in:
commit
c8d4774704
5 changed files with 172 additions and 36 deletions
|
|
@ -13,13 +13,16 @@ struct Todos {
|
||||||
scroll: scrollable::State,
|
scroll: scrollable::State,
|
||||||
input: text_input::State,
|
input: text_input::State,
|
||||||
input_value: String,
|
input_value: String,
|
||||||
|
filter: Filter,
|
||||||
tasks: Vec<Task>,
|
tasks: Vec<Task>,
|
||||||
|
controls: Controls,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
InputChanged(String),
|
InputChanged(String),
|
||||||
CreateTask,
|
CreateTask,
|
||||||
|
FilterChanged(Filter),
|
||||||
TaskMessage(usize, TaskMessage),
|
TaskMessage(usize, TaskMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +44,9 @@ impl Application for Todos {
|
||||||
self.input_value.clear();
|
self.input_value.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::FilterChanged(filter) => {
|
||||||
|
self.filter = filter;
|
||||||
|
}
|
||||||
Message::TaskMessage(i, TaskMessage::Delete) => {
|
Message::TaskMessage(i, TaskMessage::Delete) => {
|
||||||
self.tasks.remove(i);
|
self.tasks.remove(i);
|
||||||
}
|
}
|
||||||
|
|
@ -55,26 +61,39 @@ impl Application for Todos {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&mut self) -> Element<Message> {
|
fn view(&mut self) -> Element<Message> {
|
||||||
|
let Todos {
|
||||||
|
scroll,
|
||||||
|
input,
|
||||||
|
input_value,
|
||||||
|
filter,
|
||||||
|
tasks,
|
||||||
|
controls,
|
||||||
|
} = self;
|
||||||
|
|
||||||
let title = Text::new("todos")
|
let title = Text::new("todos")
|
||||||
.size(100)
|
.size(100)
|
||||||
.color(GRAY)
|
.color([0.5, 0.5, 0.5])
|
||||||
.horizontal_alignment(HorizontalAlignment::Center);
|
.horizontal_alignment(HorizontalAlignment::Center);
|
||||||
|
|
||||||
let input = TextInput::new(
|
let input = TextInput::new(
|
||||||
&mut self.input,
|
input,
|
||||||
"What needs to be done?",
|
"What needs to be done?",
|
||||||
&self.input_value,
|
input_value,
|
||||||
Message::InputChanged,
|
Message::InputChanged,
|
||||||
)
|
)
|
||||||
.padding(15)
|
.padding(15)
|
||||||
.size(30)
|
.size(30)
|
||||||
.on_submit(Message::CreateTask);
|
.on_submit(Message::CreateTask);
|
||||||
|
|
||||||
|
let controls = controls.view(&tasks, *filter);
|
||||||
|
let filtered_tasks = tasks.iter().filter(|task| filter.matches(task));
|
||||||
|
|
||||||
let tasks: Element<_> =
|
let tasks: Element<_> =
|
||||||
if self.tasks.len() > 0 {
|
if filtered_tasks.count() > 0 {
|
||||||
self.tasks
|
tasks
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.filter(|(_, task)| filter.matches(task))
|
||||||
.fold(Column::new().spacing(20), |column, (i, task)| {
|
.fold(Column::new().spacing(20), |column, (i, task)| {
|
||||||
column.push(task.view().map(move |message| {
|
column.push(task.view().map(move |message| {
|
||||||
Message::TaskMessage(i, message)
|
Message::TaskMessage(i, message)
|
||||||
|
|
@ -82,16 +101,11 @@ impl Application for Todos {
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
Container::new(
|
empty_message(match filter {
|
||||||
Text::new("You do not have any tasks! :D")
|
Filter::All => "You have not created a task yet...",
|
||||||
.size(25)
|
Filter::Active => "All your tasks are done! :D",
|
||||||
.horizontal_alignment(HorizontalAlignment::Center)
|
Filter::Completed => "You have not completed a task yet...",
|
||||||
.color([0.7, 0.7, 0.7]),
|
})
|
||||||
)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Units(200))
|
|
||||||
.center_y()
|
|
||||||
.into()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = Column::new()
|
let content = Column::new()
|
||||||
|
|
@ -99,9 +113,10 @@ impl Application for Todos {
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.push(title)
|
.push(title)
|
||||||
.push(input)
|
.push(input)
|
||||||
|
.push(controls)
|
||||||
.push(tasks);
|
.push(tasks);
|
||||||
|
|
||||||
Scrollable::new(&mut self.scroll)
|
Scrollable::new(scroll)
|
||||||
.padding(40)
|
.padding(40)
|
||||||
.push(Container::new(content).width(Length::Fill).center_x())
|
.push(Container::new(content).width(Length::Fill).center_x())
|
||||||
.into()
|
.into()
|
||||||
|
|
@ -234,18 +249,115 @@ impl Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colors
|
#[derive(Debug, Default)]
|
||||||
const GRAY: Color = Color {
|
pub struct Controls {
|
||||||
r: 0.5,
|
all_button: button::State,
|
||||||
g: 0.5,
|
active_button: button::State,
|
||||||
b: 0.5,
|
completed_button: button::State,
|
||||||
a: 1.0,
|
}
|
||||||
};
|
|
||||||
|
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).width(Length::Shrink);
|
||||||
|
let button = if filter == current_filter {
|
||||||
|
Button::new(state, label.color(Color::WHITE))
|
||||||
|
.background(Background::Color([0.2, 0.2, 0.7].into()))
|
||||||
|
} else {
|
||||||
|
Button::new(state, label)
|
||||||
|
};
|
||||||
|
|
||||||
|
button
|
||||||
|
.on_press(Message::FilterChanged(filter))
|
||||||
|
.padding(8)
|
||||||
|
.border_radius(10)
|
||||||
|
};
|
||||||
|
|
||||||
|
Row::new()
|
||||||
|
.spacing(20)
|
||||||
|
.align_items(Align::Center)
|
||||||
|
.push(
|
||||||
|
Text::new(&format!(
|
||||||
|
"{} {} left",
|
||||||
|
tasks_left,
|
||||||
|
if tasks_left == 1 { "task" } else { "tasks" }
|
||||||
|
))
|
||||||
|
.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,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
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 empty_message(message: &str) -> Element<'static, Message> {
|
||||||
|
Container::new(
|
||||||
|
Text::new(message)
|
||||||
|
.size(25)
|
||||||
|
.horizontal_alignment(HorizontalAlignment::Center)
|
||||||
|
.color([0.7, 0.7, 0.7]),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Units(200))
|
||||||
|
.center_y()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
const ICONS: Font = Font::External {
|
const ICONS: Font = Font::External {
|
||||||
name: "Icons",
|
name: "Icons",
|
||||||
bytes: include_bytes!("./resources/icons.ttf"),
|
bytes: include_bytes!("resources/icons.ttf"),
|
||||||
};
|
};
|
||||||
|
|
||||||
fn icon(unicode: char) -> Text {
|
fn icon(unicode: char) -> Text {
|
||||||
|
|
|
||||||
|
|
@ -344,11 +344,27 @@ impl Renderer {
|
||||||
for text in layer.text.iter() {
|
for text in layer.text.iter() {
|
||||||
// Target physical coordinates directly to avoid blurry text
|
// Target physical coordinates directly to avoid blurry text
|
||||||
let text = wgpu_glyph::Section {
|
let text = wgpu_glyph::Section {
|
||||||
|
// TODO: We `round` here to avoid rerasterizing text when
|
||||||
|
// its position changes slightly. This can make text feel a
|
||||||
|
// bit "jumpy". We may be able to do better once we improve
|
||||||
|
// our text rendering/caching pipeline.
|
||||||
screen_position: (
|
screen_position: (
|
||||||
(text.screen_position.0 * dpi).round(),
|
(text.screen_position.0 * dpi).round(),
|
||||||
(text.screen_position.1 * dpi).round(),
|
(text.screen_position.1 * dpi).round(),
|
||||||
),
|
),
|
||||||
bounds: (text.bounds.0 * dpi, text.bounds.1 * dpi),
|
// TODO: Fix precision issues with some DPI factors.
|
||||||
|
//
|
||||||
|
// The `ceil` here can cause some words to render on the
|
||||||
|
// same line when they should not.
|
||||||
|
//
|
||||||
|
// Ideally, `wgpu_glyph` should be able to compute layout
|
||||||
|
// using logical positions, and then apply the proper
|
||||||
|
// DPI scaling. This would ensure that both measuring and
|
||||||
|
// rendering follow the same layout rules.
|
||||||
|
bounds: (
|
||||||
|
(text.bounds.0 * dpi).ceil(),
|
||||||
|
(text.bounds.1 * dpi).ceil(),
|
||||||
|
),
|
||||||
scale: wgpu_glyph::Scale {
|
scale: wgpu_glyph::Scale {
|
||||||
x: text.scale.x * dpi,
|
x: text.scale.x * dpi,
|
||||||
y: text.scale.y * dpi,
|
y: text.scale.y * dpi,
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,15 @@ impl checkbox::Renderer for Renderer {
|
||||||
(
|
(
|
||||||
Primitive::Group {
|
Primitive::Group {
|
||||||
primitives: if checkbox.is_checked {
|
primitives: if checkbox.is_checked {
|
||||||
// TODO: Draw an actual icon
|
let check = Primitive::Text {
|
||||||
let (check, _) = text::Renderer::draw(
|
content: crate::text::CHECKMARK_ICON.to_string(),
|
||||||
self,
|
font: crate::text::BUILTIN_ICONS,
|
||||||
&Text::new("X")
|
size: checkbox_bounds.height * 0.7,
|
||||||
.horizontal_alignment(HorizontalAlignment::Center)
|
bounds: checkbox_bounds,
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
color: [0.3, 0.3, 0.3].into(),
|
||||||
checkbox_layout,
|
horizontal_alignment: HorizontalAlignment::Center,
|
||||||
);
|
vertical_alignment: VerticalAlignment::Center,
|
||||||
|
};
|
||||||
|
|
||||||
vec![checkbox_border, checkbox_box, check, label]
|
vec![checkbox_border, checkbox_box, check, label]
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ use crate::Transformation;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External {
|
||||||
|
name: "iced_wgpu icons",
|
||||||
|
bytes: include_bytes!("text/icons.ttf"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CHECKMARK_ICON: char = '\u{F00C}';
|
||||||
|
|
||||||
pub struct Pipeline {
|
pub struct Pipeline {
|
||||||
draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>,
|
draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>,
|
||||||
draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>,
|
draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>,
|
||||||
|
|
@ -91,10 +98,10 @@ impl Pipeline {
|
||||||
|
|
||||||
// TODO: This is a bit hacky. We are loading the debug font as the
|
// TODO: This is a bit hacky. We are loading the debug font as the
|
||||||
// first font in the `draw_brush`. The `measure_brush` does not
|
// first font in the `draw_brush`. The `measure_brush` does not
|
||||||
// contain this font.
|
// contain this, hence we subtract 1.
|
||||||
//
|
//
|
||||||
// This should go away once we improve the debug view and integrate
|
// This should go away once we unify `draw_brush` and
|
||||||
// it as just another UI app.
|
// `measure_brush`.
|
||||||
font_id: wgpu_glyph::FontId(font_id - 1),
|
font_id: wgpu_glyph::FontId(font_id - 1),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
BIN
wgpu/src/text/icons.ttf
Normal file
BIN
wgpu/src/text/icons.ttf
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue