From 5c34d94343538a92b0a4ee3d6b16767b6bf6950a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 14 Dec 2022 03:23:11 +0100 Subject: [PATCH 1/5] Implement vectorial text support for `Canvas` Powered by `cosmic-text` :tada: --- examples/arc/src/main.rs | 1 + examples/bezier_tool/src/main.rs | 1 + examples/clock/src/main.rs | 1 + examples/color_palette/src/main.rs | 39 +++--- examples/game_of_life/src/main.rs | 44 ++++--- examples/modern_art/src/main.rs | 1 + examples/multitouch/src/main.rs | 1 + examples/sierpinski_triangle/src/main.rs | 1 + examples/solar_system/src/main.rs | 2 + glow/src/backend.rs | 2 +- glow/src/text.rs | 2 +- graphics/Cargo.toml | 7 +- graphics/src/lib.rs | 2 +- graphics/src/renderer.rs | 11 +- graphics/src/text.rs | 28 ++++ graphics/src/{ => text}/font.rs | 4 +- graphics/src/{ => text}/font/source.rs | 2 +- graphics/src/widget/canvas.rs | 7 +- graphics/src/widget/canvas/frame.rs | 160 +++++++++++++++-------- graphics/src/widget/canvas/program.rs | 5 +- graphics/src/widget/canvas/text.rs | 3 + src/lib.rs | 1 + src/text.rs | 2 + wgpu/src/backend.rs | 2 +- wgpu/src/text.rs | 2 +- 25 files changed, 226 insertions(+), 105 deletions(-) create mode 100644 graphics/src/text.rs rename graphics/src/{ => text}/font.rs (89%) rename graphics/src/{ => text}/font/source.rs (96%) create mode 100644 src/text.rs diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 7b6ea0e186..407711e91d 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -76,6 +76,7 @@ impl canvas::Program for Arc { &self, _state: &Self::State, theme: &Theme, + _text_cache: &canvas::text::Cache, bounds: Rectangle, _cursor: Cursor, ) -> Vec { diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 7c3916d4a2..2d23967a9c 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -153,6 +153,7 @@ mod bezier { &self, state: &Self::State, _theme: &Theme, + _text_cache: &canvas::text::Cache, bounds: Rectangle, cursor: Cursor, ) -> Vec { diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index a389c54f6b..015b3c5fe4 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -90,6 +90,7 @@ impl canvas::Program for Clock { &self, _state: &Self::State, _theme: &Theme, + _text_cache: &canvas::text::Cache, bounds: Rectangle, _cursor: Cursor, ) -> Vec { diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 421499655b..788b716869 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -148,7 +148,7 @@ impl Theme { .into() } - fn draw(&self, frame: &mut Frame) { + fn draw(&self, text_cache: &canvas::text::Cache, frame: &mut Frame) { let pad = 20.0; let box_size = Size { @@ -197,14 +197,17 @@ impl Theme { }); } - frame.fill_text(canvas::Text { - content: color_hex_string(&color), - position: Point { - x: anchor.x + box_size.width / 2.0, - y: box_size.height, + frame.fill_text( + text_cache, + canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height, + }, + ..text }, - ..text - }); + ); } text.vertical_alignment = alignment::Vertical::Bottom; @@ -225,14 +228,17 @@ impl Theme { frame.fill_rectangle(anchor, box_size, color); - frame.fill_text(canvas::Text { - content: color_hex_string(&color), - position: Point { - x: anchor.x + box_size.width / 2.0, - y: box_size.height + 2.0 * pad, + frame.fill_text( + text_cache, + canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height + 2.0 * pad, + }, + ..text }, - ..text - }); + ); } } } @@ -244,11 +250,12 @@ impl canvas::Program for Theme { &self, _state: &Self::State, _theme: &iced::Theme, + text_cache: &canvas::text::Cache, bounds: Rectangle, _cursor: Cursor, ) -> Vec { let theme = self.canvas_cache.draw(bounds.size(), |frame| { - self.draw(frame); + self.draw(text_cache, frame); }); vec![theme] diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b0f1c96dba..2975c29639 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -207,9 +207,8 @@ mod grid { use iced::touch; use iced::widget::canvas; use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{ - Cache, Canvas, Cursor, Frame, Geometry, Path, Text, - }; + use iced::widget::canvas::text::{self, Text}; + use iced::widget::canvas::{Cache, Canvas, Cursor, Frame, Geometry, Path}; use iced::{ alignment, mouse, Color, Element, Length, Point, Rectangle, Size, Theme, Vector, @@ -537,6 +536,7 @@ mod grid { &self, _interaction: &Interaction, _theme: &Theme, + text_cache: &text::Cache, bounds: Rectangle, cursor: Cursor, ) -> Vec { @@ -593,32 +593,38 @@ mod grid { let text = Text { color: Color::WHITE, size: 14.0, - position: Point::new(frame.width(), frame.height()), + position: Point::new(frame.width(), frame.height() - 4.0), horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Bottom, ..Text::default() }; if let Some(cell) = hovered_cell { - frame.fill_text(Text { - content: format!("({}, {})", cell.j, cell.i), - position: text.position - Vector::new(0.0, 16.0), - ..text - }); + frame.fill_text( + text_cache, + Text { + content: format!("({}, {})", cell.j, cell.i), + position: text.position - Vector::new(0.0, 20.0), + ..text + }, + ); } let cell_count = self.state.cell_count(); - frame.fill_text(Text { - content: format!( - "{} cell{} @ {:?} ({})", - cell_count, - if cell_count == 1 { "" } else { "s" }, - self.last_tick_duration, - self.last_queued_ticks - ), - ..text - }); + frame.fill_text( + text_cache, + Text { + content: format!( + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }, + ); frame.into_geometry() }; diff --git a/examples/modern_art/src/main.rs b/examples/modern_art/src/main.rs index 28ed3e215c..0c6c72c884 100644 --- a/examples/modern_art/src/main.rs +++ b/examples/modern_art/src/main.rs @@ -62,6 +62,7 @@ impl canvas::Program for ModernArt { &self, _state: &Self::State, _theme: &Theme, + _text_cache: &canvas::text::Cache, bounds: Rectangle, _cursor: Cursor, ) -> Vec { diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index f5faae0f4a..5dcaed92fd 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -126,6 +126,7 @@ impl canvas::Program for State { &self, _state: &Self::State, _theme: &Theme, + _text_cache: &canvas::text::Cache, bounds: Rectangle, _cursor: Cursor, ) -> Vec { diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 1d25d17166..eba313361e 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -135,6 +135,7 @@ impl canvas::Program for SierpinskiGraph { &self, _state: &Self::State, _theme: &Theme, + _text_cache: &canvas::text::Cache, bounds: Rectangle, _cursor: canvas::Cursor, ) -> Vec { diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9e303576f9..287dc3ea16 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -13,6 +13,7 @@ use iced::time; use iced::widget::canvas; use iced::widget::canvas::gradient::{self, Gradient}; use iced::widget::canvas::stroke::{self, Stroke}; +use iced::widget::canvas::text; use iced::widget::canvas::{Cursor, Path}; use iced::window; use iced::{ @@ -158,6 +159,7 @@ impl canvas::Program for State { &self, _state: &Self::State, _theme: &Theme, + _text_cache: &text::Cache, bounds: Rectangle, _cursor: Cursor, ) -> Vec { diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 416c3b94d4..2687daed3a 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -6,7 +6,7 @@ use crate::{program, triangle}; use crate::{Settings, Transformation, Viewport}; use iced_graphics::backend; -use iced_graphics::font; +use iced_graphics::text::font; use iced_graphics::{Layer, Primitive}; use iced_native::alignment; use iced_native::{Font, Size}; diff --git a/glow/src/text.rs b/glow/src/text.rs index 37ccdece0d..8f8748b4c1 100644 --- a/glow/src/text.rs +++ b/glow/src/text.rs @@ -1,6 +1,6 @@ use crate::Transformation; -use iced_graphics::font; +use iced_graphics::text::font; use glow_glyph::ab_glyph; use std::{cell::RefCell, collections::HashMap}; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 823a05f436..c74db81cee 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -24,7 +24,7 @@ bmp = ["image_rs/bmp"] hdr = ["image_rs/hdr"] dds = ["image_rs/dds"] farbfeld = ["image_rs/farbfeld"] -canvas = ["lyon"] +canvas = ["lyon", "cosmic-text"] qr_code = ["qrcode", "canvas"] font-source = ["font-kit"] font-fallback = [] @@ -38,6 +38,7 @@ log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" bitflags = "1.2" +once_cell = "1.0" [dependencies.bytemuck] version = "1.4" @@ -55,6 +56,10 @@ path = "../style" version = "1.0" optional = true +[dependencies.cosmic-text] +version = "0.5.5" +optional = true + [dependencies.qrcode] version = "0.12" optional = true diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index d39dd90ce6..800f1adda8 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -28,12 +28,12 @@ mod transformation; mod viewport; pub mod backend; -pub mod font; pub mod gradient; pub mod image; pub mod layer; pub mod overlay; pub mod renderer; +pub mod text; pub mod triangle; pub mod widget; pub mod window; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index aabdf7fc45..6404da0451 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,11 +1,11 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; +use crate::text::{self, Text}; use crate::{Primitive, Vector}; use iced_native::image; use iced_native::layout; use iced_native::renderer; use iced_native::svg; -use iced_native::text::{self, Text}; use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; pub use iced_native::renderer::Style; @@ -13,10 +13,11 @@ pub use iced_native::renderer::Style; use std::marker::PhantomData; /// A backend-agnostic renderer that supports all the built-in widgets. -#[derive(Debug)] +#[allow(missing_debug_implementations)] pub struct Renderer { backend: B, primitives: Vec, + text_cache: text::Cache, theme: PhantomData, } @@ -26,6 +27,7 @@ impl Renderer { Self { backend, primitives: Vec::new(), + text_cache: text::Cache::new(), theme: PhantomData, } } @@ -45,6 +47,11 @@ impl Renderer { pub fn with_primitives(&mut self, f: impl FnOnce(&mut B, &[Primitive])) { f(&mut self.backend, &self.primitives); } + + /// Returns a reference to the current [`text::Cache`]. + pub fn text_cache(&self) -> &text::Cache { + &self.text_cache + } } impl iced_native::Renderer for Renderer diff --git a/graphics/src/text.rs b/graphics/src/text.rs new file mode 100644 index 0000000000..7a894fc665 --- /dev/null +++ b/graphics/src/text.rs @@ -0,0 +1,28 @@ +//! Draw text for your users. +pub mod font; + +pub use iced_native::text::{self, *}; + +use std::cell::RefCell; + +#[cfg(feature = "canvas")] +/// An access to system fonts. +pub static FONT_SYSTEM: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| cosmic_text::FontSystem::new()); + +/// A text cache. +#[allow(missing_debug_implementations)] +pub struct Cache { + #[cfg(feature = "canvas")] + pub(crate) swash: RefCell>, +} + +impl Cache { + /// Creates a new text [`Cache`]. + pub fn new() -> Self { + Self { + #[cfg(feature = "canvas")] + swash: RefCell::new(cosmic_text::SwashCache::new(&FONT_SYSTEM)), + } + } +} diff --git a/graphics/src/font.rs b/graphics/src/text/font.rs similarity index 89% rename from graphics/src/font.rs rename to graphics/src/text/font.rs index d55d0faf5f..45c8c96e5c 100644 --- a/graphics/src/font.rs +++ b/graphics/src/text/font.rs @@ -15,14 +15,14 @@ pub use font_kit::{ /// A built-in fallback font, for convenience. #[cfg(feature = "font-fallback")] #[cfg_attr(docsrs, doc(cfg(feature = "font-fallback")))] -pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); +pub const FALLBACK: &[u8] = include_bytes!("../../fonts/Lato-Regular.ttf"); /// A built-in icon font, for convenience. #[cfg(feature = "font-icons")] #[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] pub const ICONS: iced_native::Font = iced_native::Font::External { name: "iced_wgpu icons", - bytes: include_bytes!("../fonts/Icons.ttf"), + bytes: include_bytes!("../../fonts/Icons.ttf"), }; /// The `char` representing a ✔ icon in the built-in [`ICONS`] font. diff --git a/graphics/src/font/source.rs b/graphics/src/text/font/source.rs similarity index 96% rename from graphics/src/font/source.rs rename to graphics/src/text/font/source.rs index c0b50e1dda..8efe2a370d 100644 --- a/graphics/src/font/source.rs +++ b/graphics/src/text/font/source.rs @@ -1,4 +1,4 @@ -use crate::font::{Family, LoadError}; +use crate::text::font::{Family, LoadError}; /// A font source that can find and load system fonts. #[allow(missing_debug_implementations)] diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index b070d0a676..79e38f0cb4 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -7,6 +7,7 @@ pub mod event; pub mod fill; pub mod path; pub mod stroke; +pub mod text; mod cache; mod cursor; @@ -14,7 +15,6 @@ mod frame; mod geometry; mod program; mod style; -mod text; pub use crate::gradient::{self, Gradient}; pub use cache::Cache; @@ -53,6 +53,7 @@ use std::marker::PhantomData; /// # } /// # pub use iced_native::{Color, Rectangle, Theme}; /// # } +/// use iced::widget::canvas::text; /// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; /// use iced::{Color, Rectangle, Theme}; /// @@ -66,7 +67,7 @@ use std::marker::PhantomData; /// impl Program<()> for Circle { /// type State = (); /// -/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// fn draw(&self, _state: &(), _theme: &Theme, _text_cache: &text::Cache, bounds: Rectangle, _cursor: Cursor) -> Vec{ /// // We prepare a new `Frame` /// let mut frame = Frame::new(bounds.size()); /// @@ -243,7 +244,7 @@ where renderer.draw_primitive(Primitive::Group { primitives: self .program - .draw(state, theme, bounds, cursor) + .draw(state, theme, renderer.text_cache(), bounds, cursor) .into_iter() .map(Geometry::into_primitive) .collect(), diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index d68548ae8d..756d07fcaa 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -1,9 +1,11 @@ +use crate::alignment; use crate::gradient::Gradient; +use crate::text; use crate::triangle; use crate::widget::canvas::{path, Fill, Geometry, Path, Stroke, Style, Text}; use crate::Primitive; -use iced_native::{Point, Rectangle, Size, Vector}; +use iced_native::{Font, Point, Rectangle, Size, Vector}; use lyon::geom::euclid; use lyon::tessellation; @@ -295,46 +297,111 @@ impl Frame { /// Draws the characters of the given [`Text`] on the [`Frame`], filling /// them with the given color. /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { + pub fn fill_text(&mut self, cache: &text::Cache, text: impl Into) { let text = text.into(); - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); + let metrics = + cosmic_text::Metrics::new(text.size as i32, text.size as i32); + let attrs = match text.font { + Font::Default => cosmic_text::Attrs::new(), + Font::External { name, .. } => cosmic_text::Attrs { + family: cosmic_text::Family::Name(name), + ..cosmic_text::Attrs::new() + }, + }; - Point::new(transformed.x, transformed.y) + let mut buffer = cosmic_text::BufferLine::new( + &text.content, + cosmic_text::AttrsList::new(attrs), + ); + + let layout = + buffer.layout(&text::FONT_SYSTEM, metrics.font_size, i32::MAX); + + let translation_x = match text.horizontal_alignment { + alignment::Horizontal::Left => text.position.x, + alignment::Horizontal::Center | alignment::Horizontal::Right => { + let mut line_width = 0.0f32; + + for line in layout.iter() { + line_width = line_width.max(line.w); + } + + if text.horizontal_alignment == alignment::Horizontal::Center { + text.position.x - line_width / 2.0 + } else { + text.position.x - line_width + } + } }; - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); + let translation_y = { + let total_height = text.size * layout.len() as f32; + + match text.vertical_alignment { + alignment::Vertical::Top => text.position.y, + alignment::Vertical::Center => { + text.position.y + total_height / 2.0 + } + alignment::Vertical::Bottom => text.position.y + total_height, + } + }; + + for run in layout.iter() { + for glyph in run.glyphs.iter() { + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y as f32 + glyph.y_offset - text.size; + + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = cache + .swash + .borrow_mut() + .get_outline_commands(glyph.cache_key) + { + let glyph = Path::new(|path| { + use cosmic_text::Command; + + for command in commands { + match command { + Command::MoveTo(p) => { + path.move_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::LineTo(p) => { + path.line_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::CurveTo(control_a, control_b, to) => { + path.bezier_curve_to( + Point::new(control_a.x, -control_a.y) + + offset, + Point::new(control_b.x, -control_b.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::QuadTo(control, to) => { + path.quadratic_curve_to( + Point::new(control.x, -control.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::Close => { + path.close(); + } + } + } + }); + + self.fill(&glyph, text.color); + } + } + } } /// Stores the current transform of the [`Frame`] and executes the given @@ -365,28 +432,11 @@ impl Frame { let primitives = frame.into_primitives(); - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - let translation = Vector::new(region.x, region.y); - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(region.size()), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], + self.primitives.push(Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives }), }); } diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index 656dbfa627..d0d118372f 100644 --- a/graphics/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -1,3 +1,4 @@ +use crate::text; use crate::widget::canvas::event::{self, Event}; use crate::widget::canvas::mouse; use crate::widget::canvas::{Cursor, Geometry}; @@ -45,6 +46,7 @@ pub trait Program { &self, state: &Self::State, theme: &Theme, + text_cache: &text::Cache, bounds: Rectangle, cursor: Cursor, ) -> Vec; @@ -85,10 +87,11 @@ where &self, state: &Self::State, theme: &Theme, + text_cache: &text::Cache, bounds: Rectangle, cursor: Cursor, ) -> Vec { - T::draw(self, state, theme, bounds, cursor) + T::draw(self, state, theme, text_cache, bounds, cursor) } fn mouse_interaction( diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs index 056f8204c7..357dd726c0 100644 --- a/graphics/src/widget/canvas/text.rs +++ b/graphics/src/widget/canvas/text.rs @@ -1,6 +1,9 @@ +//! Draw text in a canvas. use crate::alignment; use crate::{Color, Font, Point}; +pub use crate::text::Cache; + /// A bunch of text that can be drawn to a canvas #[derive(Debug, Clone)] pub struct Text { diff --git a/src/lib.rs b/src/lib.rs index 001768272b..6bdbf32186 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,6 +176,7 @@ pub mod keyboard; pub mod mouse; pub mod overlay; pub mod settings; +pub mod text; pub mod time; pub mod touch; pub mod widget; diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000000..f6fcd04a7a --- /dev/null +++ b/src/text.rs @@ -0,0 +1,2 @@ +//! Draw text for your users. +pub use iced_graphics::text::Cache; diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 946eb71290..9af06a9765 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -4,8 +4,8 @@ use crate::triangle; use crate::{Settings, Transformation}; use iced_graphics::backend; -use iced_graphics::font; use iced_graphics::layer::Layer; +use iced_graphics::text::font; use iced_graphics::{Primitive, Viewport}; use iced_native::alignment; use iced_native::{Font, Size}; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e17b84c15d..069a95f79a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,6 +1,6 @@ use crate::Transformation; -use iced_graphics::font; +use iced_graphics::text::font; use std::{cell::RefCell, collections::HashMap}; use wgpu_glyph::ab_glyph; From 4a0a39a718e79d20fa3b77654022d96dec06911f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 20 Dec 2022 10:42:53 +0100 Subject: [PATCH 2/5] Add `vectorial_text` example --- examples/vectorial_text/Cargo.toml | 9 ++ examples/vectorial_text/src/main.rs | 172 ++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 examples/vectorial_text/Cargo.toml create mode 100644 examples/vectorial_text/src/main.rs diff --git a/examples/vectorial_text/Cargo.toml b/examples/vectorial_text/Cargo.toml new file mode 100644 index 0000000000..76c1af7cc3 --- /dev/null +++ b/examples/vectorial_text/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "vectorial_text" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "debug"] } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs new file mode 100644 index 0000000000..4ebe3d6018 --- /dev/null +++ b/examples/vectorial_text/src/main.rs @@ -0,0 +1,172 @@ +use iced::alignment::{self, Alignment}; +use iced::widget::canvas::{Cursor, Frame, Text}; +use iced::widget::{ + canvas, checkbox, column, horizontal_space, row, slider, text, +}; +use iced::{ + Color, Element, Font, Length, Point, Rectangle, Sandbox, Settings, Theme, + Vector, +}; + +pub fn main() -> iced::Result { + VectorialText::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct VectorialText { + state: State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + SizeChanged(f32), + AngleChanged(f32), + ScaleChanged(f32), + ToggleJapanese(bool), +} + +impl Sandbox for VectorialText { + type Message = Message; + + fn new() -> Self { + Self { + state: State::new(), + } + } + + fn title(&self) -> String { + String::from("Vectorial Text - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::SizeChanged(size) => { + self.state.size = size; + } + Message::AngleChanged(angle) => { + self.state.angle = angle; + } + Message::ScaleChanged(scale) => { + self.state.scale = scale; + } + Message::ToggleJapanese(use_japanese) => { + self.state.use_japanese = use_japanese; + } + } + } + + fn view(&self) -> Element { + let slider_with_label = |label, range, value, message: fn(f32) -> _| { + column![ + row![ + text(label), + horizontal_space(Length::Fill), + text(format!("{:.2}", value)) + ], + slider(range, value, message).step(0.01) + ] + .width(Length::Fill) + .spacing(2) + }; + + column![ + canvas(&self.state).width(Length::Fill).height(Length::Fill), + column![ + checkbox( + "Use Japanese", + self.state.use_japanese, + Message::ToggleJapanese + ), + row![ + slider_with_label( + "Size", + 2.0..=80.0, + self.state.size, + Message::SizeChanged, + ), + slider_with_label( + "Angle", + 0.0..=360.0, + self.state.angle, + Message::AngleChanged, + ), + slider_with_label( + "Scale", + 1.0..=20.0, + self.state.scale, + Message::ScaleChanged, + ), + ] + .spacing(20), + ] + .align_items(Alignment::Center) + .spacing(10) + ] + .spacing(10) + .padding(20) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } +} + +struct State { + size: f32, + angle: f32, + scale: f32, + use_japanese: bool, +} + +impl State { + pub fn new() -> Self { + Self { + size: 40.0, + angle: 0.0, + scale: 1.0, + use_japanese: false, + } + } +} + +impl canvas::Program for State { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + text_cache: &canvas::text::Cache, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { + let mut frame = Frame::new(bounds.size()); + let center = bounds.center(); + + frame.translate(Vector::new(center.x, center.y)); + frame.scale(self.scale); + frame.rotate(self.angle * std::f32::consts::PI / 180.0); + + frame.fill_text( + text_cache, + Text { + position: Point::new(0.0, self.size), + color: Color::WHITE, + font: Font::Default, + size: self.size, + content: String::from(if self.use_japanese { + "ベクトルテキスト" + } else { + "Vectorial Text!" + }), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + }, + ); + + vec![frame.into_geometry()] + } +} From f150e2d40b189c334f8846f2642c560c89d2f142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 20 Dec 2022 10:55:36 +0100 Subject: [PATCH 3/5] Fall back to bitmap if glyph outline is `None` in `Canvas` --- examples/vectorial_text/src/main.rs | 4 ++-- graphics/src/widget/canvas/frame.rs | 30 ++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 4ebe3d6018..4600be8198 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -158,9 +158,9 @@ impl canvas::Program for State { font: Font::Default, size: self.size, content: String::from(if self.use_japanese { - "ベクトルテキスト" + "ベクトルテキスト🎉" } else { - "Vectorial Text!" + "Vectorial Text! 🎉" }), horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 756d07fcaa..9ef6150f8a 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -5,7 +5,7 @@ use crate::triangle; use crate::widget::canvas::{path, Fill, Geometry, Path, Stroke, Style, Text}; use crate::Primitive; -use iced_native::{Font, Point, Rectangle, Size, Vector}; +use iced_native::{Color, Font, Point, Rectangle, Size, Vector}; use lyon::geom::euclid; use lyon::tessellation; @@ -355,10 +355,10 @@ impl Frame { let offset = Vector::new(start_x, start_y); - if let Some(commands) = cache - .swash - .borrow_mut() - .get_outline_commands(glyph.cache_key) + let mut swash_cache = cache.swash.borrow_mut(); + + if let Some(commands) = + swash_cache.get_outline_commands(glyph.cache_key) { let glyph = Path::new(|path| { use cosmic_text::Command; @@ -399,6 +399,26 @@ impl Frame { }); self.fill(&glyph, text.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = text.color.into_rgba8(); + + swash_cache.with_pixels( + glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + self.fill_rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + Color::from_rgba8( + color.r(), + color.g(), + color.b(), + color.a() as f32 / 255.0, + ), + ); + }, + ) } } } From c023ecd04fe327a1745168ea4d4ecdc87f0c8334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 20 Dec 2022 11:59:29 +0100 Subject: [PATCH 4/5] Fix `clippy` lints --- graphics/src/text.rs | 8 +++++++- graphics/src/widget/canvas/frame.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 7a894fc665..409c33fcee 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -8,7 +8,7 @@ use std::cell::RefCell; #[cfg(feature = "canvas")] /// An access to system fonts. pub static FONT_SYSTEM: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(|| cosmic_text::FontSystem::new()); + once_cell::sync::Lazy::new(cosmic_text::FontSystem::new); /// A text cache. #[allow(missing_debug_implementations)] @@ -26,3 +26,9 @@ impl Cache { } } } + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 9ef6150f8a..b5301b92de 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -351,7 +351,7 @@ impl Frame { for run in layout.iter() { for glyph in run.glyphs.iter() { let start_x = translation_x + glyph.x + glyph.x_offset; - let start_y = translation_y as f32 + glyph.y_offset - text.size; + let start_y = translation_y + glyph.y_offset - text.size; let offset = Vector::new(start_x, start_y); From bc78a6c1b22c945cbd5399cbcdc05f56791f73ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 20 Dec 2022 12:36:18 +0100 Subject: [PATCH 5/5] Use `fill` instead of `fill_rectangle` for bitmap glyphs in `Canvas` `fill_rectangle` can only be used with axis-aligned rectangles. --- graphics/src/widget/canvas/frame.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index b5301b92de..4bb99d971e 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -407,9 +407,11 @@ impl Frame { glyph.cache_key, cosmic_text::Color::rgba(r, g, b, a), |x, y, color| { - self.fill_rectangle( - Point::new(x as f32, y as f32) + offset, - Size::new(1.0, 1.0), + self.fill( + &Path::rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + ), Color::from_rgba8( color.r(), color.g(),