diff options
author | 2019-11-14 06:48:32 +0100 | |
---|---|---|
committer | 2019-11-14 06:48:32 +0100 | |
commit | c8d4774704f970bb26eb5d9903b7a741b1c225e3 (patch) | |
tree | 1ceabb28d98c4f4cc9a556d2a5328ce8b7163130 | |
parent | af5ec4941270c832557994d9b4cc70ce5feac911 (diff) | |
parent | 2c8ba652a7929ac6c2af28ac60a8bd4b8e8e2f10 (diff) | |
download | iced-c8d4774704f970bb26eb5d9903b7a741b1c225e3.tar.gz iced-c8d4774704f970bb26eb5d9903b7a741b1c225e3.tar.bz2 iced-c8d4774704f970bb26eb5d9903b7a741b1c225e3.zip |
Merge pull request #56 from hecrj/example/filter-todos
Draw checkmark icon and filter todos
-rw-r--r-- | examples/todos.rs | 160 | ||||
-rw-r--r-- | wgpu/src/renderer.rs | 18 | ||||
-rw-r--r-- | wgpu/src/renderer/widget/checkbox.rs | 17 | ||||
-rw-r--r-- | wgpu/src/text.rs | 13 | ||||
-rw-r--r-- | wgpu/src/text/icons.ttf | bin | 0 -> 4912 bytes |
5 files changed, 172 insertions, 36 deletions
diff --git a/examples/todos.rs b/examples/todos.rs index 028b2d65..f921a666 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -13,13 +13,16 @@ struct Todos { scroll: scrollable::State, input: text_input::State, input_value: String, + filter: Filter, tasks: Vec<Task>, + controls: Controls, } #[derive(Debug, Clone)] pub enum Message { InputChanged(String), CreateTask, + FilterChanged(Filter), TaskMessage(usize, TaskMessage), } @@ -41,6 +44,9 @@ impl Application for Todos { self.input_value.clear(); } } + Message::FilterChanged(filter) => { + self.filter = filter; + } Message::TaskMessage(i, TaskMessage::Delete) => { self.tasks.remove(i); } @@ -55,26 +61,39 @@ impl Application for Todos { } fn view(&mut self) -> Element<Message> { + let Todos { + scroll, + input, + input_value, + filter, + tasks, + controls, + } = self; + let title = Text::new("todos") .size(100) - .color(GRAY) + .color([0.5, 0.5, 0.5]) .horizontal_alignment(HorizontalAlignment::Center); let input = TextInput::new( - &mut self.input, + input, "What needs to be done?", - &self.input_value, + 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 self.tasks.len() > 0 { - self.tasks + 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) @@ -82,16 +101,11 @@ impl Application for Todos { }) .into() } else { - Container::new( - Text::new("You do not have any tasks! :D") - .size(25) - .horizontal_alignment(HorizontalAlignment::Center) - .color([0.7, 0.7, 0.7]), - ) - .width(Length::Fill) - .height(Length::Units(200)) - .center_y() - .into() + 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() @@ -99,9 +113,10 @@ impl Application for Todos { .spacing(20) .push(title) .push(input) + .push(controls) .push(tasks); - Scrollable::new(&mut self.scroll) + Scrollable::new(scroll) .padding(40) .push(Container::new(content).width(Length::Fill).center_x()) .into() @@ -234,18 +249,115 @@ impl Task { } } -// Colors -const GRAY: Color = Color { - r: 0.5, - g: 0.5, - b: 0.5, - a: 1.0, -}; +#[derive(Debug, Default)] +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).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 const ICONS: Font = Font::External { name: "Icons", - bytes: include_bytes!("./resources/icons.ttf"), + bytes: include_bytes!("resources/icons.ttf"), }; fn icon(unicode: char) -> Text { diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs index d3bdc878..52764248 100644 --- a/wgpu/src/renderer.rs +++ b/wgpu/src/renderer.rs @@ -344,11 +344,27 @@ impl Renderer { for text in layer.text.iter() { // Target physical coordinates directly to avoid blurry text 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: ( (text.screen_position.0 * 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 { x: text.scale.x * dpi, y: text.scale.y * dpi, diff --git a/wgpu/src/renderer/widget/checkbox.rs b/wgpu/src/renderer/widget/checkbox.rs index c2d7911c..aedb821c 100644 --- a/wgpu/src/renderer/widget/checkbox.rs +++ b/wgpu/src/renderer/widget/checkbox.rs @@ -74,14 +74,15 @@ impl checkbox::Renderer for Renderer { ( Primitive::Group { primitives: if checkbox.is_checked { - // TODO: Draw an actual icon - let (check, _) = text::Renderer::draw( - self, - &Text::new("X") - .horizontal_alignment(HorizontalAlignment::Center) - .vertical_alignment(VerticalAlignment::Center), - checkbox_layout, - ); + let check = Primitive::Text { + content: crate::text::CHECKMARK_ICON.to_string(), + font: crate::text::BUILTIN_ICONS, + size: checkbox_bounds.height * 0.7, + bounds: checkbox_bounds, + color: [0.3, 0.3, 0.3].into(), + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + }; vec![checkbox_border, checkbox_box, check, label] } else { diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 3205fe55..da070f5c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -5,6 +5,13 @@ use crate::Transformation; use std::cell::RefCell; 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 { draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>, 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 // 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 - // it as just another UI app. + // This should go away once we unify `draw_brush` and + // `measure_brush`. font_id: wgpu_glyph::FontId(font_id - 1), ..Default::default() }; diff --git a/wgpu/src/text/icons.ttf b/wgpu/src/text/icons.ttf Binary files differnew file mode 100644 index 00000000..1c832f86 --- /dev/null +++ b/wgpu/src/text/icons.ttf |