From 00479d8bcdf619dab8a3eaa47ff551c6c3a0ac1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 14 Nov 2019 03:00:33 +0100 Subject: [PATCH 1/3] Fix text bounds in `iced_wgpu` on nonintegral DPI --- wgpu/src/renderer.rs | 18 +++++++++++++++++- wgpu/src/text.rs | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) 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/text.rs b/wgpu/src/text.rs index 3205fe55..81fc1fb5 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -91,7 +91,7 @@ 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 font, hence we subtract 1. // // This should go away once we improve the debug view and integrate // it as just another UI app. From 66bb2f7cbffafd75bbba24246dc38cd0d0c4744e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 14 Nov 2019 03:03:50 +0100 Subject: [PATCH 2/3] Add filter controls to `todos` example :tada: --- examples/todos.rs | 158 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 23 deletions(-) diff --git a/examples/todos.rs b/examples/todos.rs index 028b2d65..19e2a701 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, + 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 { + 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,13 +249,110 @@ 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 { + 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 { From 2c8ba652a7929ac6c2af28ac60a8bd4b8e8e2f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 14 Nov 2019 03:34:41 +0100 Subject: [PATCH 3/3] Draw proper checkmark for `Checkbox` in `iced_wgpu` --- examples/todos.rs | 2 +- wgpu/src/renderer/widget/checkbox.rs | 17 +++++++++-------- wgpu/src/text.rs | 13 ++++++++++--- wgpu/src/text/icons.ttf | Bin 0 -> 4912 bytes 4 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 wgpu/src/text/icons.ttf diff --git a/examples/todos.rs b/examples/todos.rs index 19e2a701..f921a666 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -357,7 +357,7 @@ fn empty_message(message: &str) -> Element<'static, Message> { // 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/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 81fc1fb5..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>, draw_font_map: RefCell>, @@ -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, hence we subtract 1. + // 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 new file mode 100644 index 0000000000000000000000000000000000000000..1c832f86576e51451b729a7fe513616dea38d21a GIT binary patch literal 4912 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 XW9RmOAqK|oi2Gkufv@64F+=oU3sq^e literal 0 HcmV?d00001