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..d366b9072c --- /dev/null +++ b/examples/vectorial_text/src/main.rs @@ -0,0 +1,175 @@ +use iced::alignment::{self, Alignment}; +use iced::mouse; +use iced::widget::{ + canvas, checkbox, column, horizontal_space, row, slider, text, +}; +use iced::{ + Element, Length, Point, Rectangle, Renderer, 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; + } + } + + self.state.cache.clear(); + } + + 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) + ] + .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, + cache: canvas::Cache, +} + +impl State { + pub fn new() -> Self { + Self { + size: 40.0, + angle: 0.0, + scale: 1.0, + use_japanese: false, + cache: canvas::Cache::new(), + } + } +} + +impl canvas::Program for State { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { + let palette = theme.palette(); + 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(canvas::Text { + position: Point::new(0.0, 0.0), + color: palette.text, + size: self.size.into(), + content: String::from(if self.use_japanese { + "ベクトルテキスト🎉" + } else { + "Vectorial Text! 🎉" + }), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + ..canvas::Text::default() + }); + }); + + vec![geometry] + } +} diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 0bf7ec9794..d314e85ec9 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,6 +1,8 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point}; +use crate::core::{Color, Font, Pixels, Point, Size, Vector}; +use crate::geometry::Path; +use crate::text; /// A bunch of text that can be drawn to a canvas #[derive(Debug, Clone)] @@ -32,6 +34,137 @@ pub struct Text { pub shaping: Shaping, } +impl Text { + /// Computes the [`Path`]s of the [`Text`] and draws them using + /// the given closure. + pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::BufferLine::new( + &self.content, + cosmic_text::AttrsList::new(text::to_attributes(self.font)), + text::to_shaping(self.shaping), + ); + + let layout = buffer.layout( + font_system.raw(), + self.size.0, + f32::MAX, + cosmic_text::Wrap::None, + ); + + let translation_x = match self.horizontal_alignment { + alignment::Horizontal::Left => self.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 self.horizontal_alignment == alignment::Horizontal::Center { + self.position.x - line_width / 2.0 + } else { + self.position.x - line_width + } + } + }; + + let translation_y = { + let line_height = self.line_height.to_absolute(self.size); + + match self.vertical_alignment { + alignment::Vertical::Top => self.position.y, + alignment::Vertical::Center => { + self.position.y - line_height.0 / 2.0 + } + alignment::Vertical::Bottom => self.position.y - line_height.0, + } + }; + + let mut swash_cache = cosmic_text::SwashCache::new(); + + for run in layout.iter() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y + glyph.y_offset + self.size.0; + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = swash_cache.get_outline_commands( + font_system.raw(), + physical_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(); + } + } + } + }); + + f(glyph, self.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = self.color.into_rgba8(); + + swash_cache.with_pixels( + font_system.raw(), + physical_glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + f( + Path::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, + ), + ); + }, + ); + } + } + } + } +} + impl Default for Text { fn default() -> Text { Text { diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 706db40e50..d1393b4d7a 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -543,7 +543,6 @@ impl Backend { path, paint, rule, - transform, }) => { let bounds = path.bounds(); @@ -566,9 +565,11 @@ impl Backend { path, paint, *rule, - transform - .post_translate(translation.x, translation.y) - .post_scale(scale_factor, scale_factor), + tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor), clip_mask, ); } @@ -576,7 +577,6 @@ impl Backend { path, paint, stroke, - transform, }) => { let bounds = path.bounds(); @@ -599,9 +599,11 @@ impl Backend { path, paint, stroke, - transform - .post_translate(translation.x, translation.y) - .post_scale(scale_factor, scale_factor), + tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor), clip_mask, ); } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 5f28b7373a..74a08d384a 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,4 +1,5 @@ -use crate::core::{Point, Rectangle, Size, Vector}; +use crate::core::text::LineHeight; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{Path, Style, Text}; @@ -39,17 +40,22 @@ impl Frame { } pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Some(path) = convert_path(path) else { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { return; }; + let fill = fill.into(); + let mut paint = into_paint(fill.style); + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Fill { path, - paint: into_paint(fill.style), + paint, rule: into_fill_rule(fill.rule), - transform: self.transform, })); } @@ -59,76 +65,111 @@ impl Frame { size: Size, fill: impl Into, ) { - let Some(path) = convert_path(&Path::rectangle(top_left, size)) else { + let Some(path) = convert_path(&Path::rectangle(top_left, size)) + .and_then(|path| path.transform(self.transform)) + else { return; }; + let fill = fill.into(); + let mut paint = tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }; + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Fill { path, - paint: tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }, + paint, rule: into_fill_rule(fill.rule), - transform: self.transform, })); } pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let Some(path) = convert_path(path) else { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { return; }; let stroke = stroke.into(); let skia_stroke = into_stroke(&stroke); + let mut paint = into_paint(stroke.style); + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Stroke { path, - paint: into_paint(stroke.style), + paint, stroke: skia_stroke, - transform: self.transform, })); } pub fn fill_text(&mut self, text: impl Into) { let text = text.into(); - let position = if self.transform.is_identity() { - text.position + let (scale_x, scale_y) = self.transform.get_scale(); + + if self.transform.is_scale_translate() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = if self.transform.is_identity() + { + (text.position, text.size, text.line_height) + } else { + let mut position = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut position); + + let size = text.size.0 * scale_y; + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + ( + Point::new(position[0].x, position[0].y), + size.into(), + line_height, + ) + }; + + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }; + + // TODO: Honor layering! + self.primitives.push(Primitive::Text { + content: text.content, + bounds, + color: text.color, + size, + line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITY), + }); } else { - let mut transformed = [tiny_skia::Point { - x: text.position.x, - y: text.position.y, - }]; - - self.transform.map_points(&mut transformed); - - Point::new(transformed[0].x, transformed[0].y) - }; - - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds, - color: text.color, - size: text.size, - line_height: text.line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }); + text.draw_with(|path, color| self.fill(&path, color)); + } } pub fn push_transform(&mut self) { diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index 0ed2496902..7718d54297 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -13,8 +13,6 @@ pub enum Custom { paint: tiny_skia::Paint<'static>, /// The fill rule to follow. rule: tiny_skia::FillRule, - /// The transform to apply to the path. - transform: tiny_skia::Transform, }, /// A path stroked with some paint. Stroke { @@ -24,8 +22,6 @@ pub enum Custom { paint: tiny_skia::Paint<'static>, /// The stroke settings. stroke: tiny_skia::Stroke, - /// The transform to apply to the path. - transform: tiny_skia::Transform, }, } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index e0bff67efa..4d7f443ea2 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,5 +1,6 @@ //! Build and draw geometry. -use crate::core::{Point, Rectangle, Size, Vector}; +use crate::core::text::LineHeight; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -115,19 +116,31 @@ struct Transforms { } #[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} +struct Transform(lyon::math::Transform); impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { + fn is_identity(&self) -> bool { + self.0 == lyon::math::Transform::identity() + } + + fn is_scale_translation(&self) -> bool { + self.0.m12.abs() < 2.0 * f32::EPSILON + && self.0.m21.abs() < 2.0 * f32::EPSILON + } + + fn scale(&self) -> (f32, f32) { + (self.0.m11, self.0.m22) + } + + fn transform_point(&self, point: Point) -> Point { let transformed = self - .raw + .0 .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; + + Point { + x: transformed.x, + y: transformed.y, + } } fn transform_style(&self, style: Style) -> Style { @@ -142,8 +155,8 @@ impl Transform { fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { match &mut gradient { Gradient::Linear(linear) => { - self.transform_point(&mut linear.start); - self.transform_point(&mut linear.end); + linear.start = self.transform_point(linear.start); + linear.end = self.transform_point(linear.end); } } @@ -163,10 +176,7 @@ impl Frame { primitives: Vec::new(), transforms: Transforms { previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, + current: Transform(lyon::math::Transform::identity()), }, fill_tessellator: tessellation::FillTessellator::new(), stroke_tessellator: tessellation::StrokeTessellator::new(), @@ -209,14 +219,14 @@ impl Frame { let options = tessellation::FillOptions::default() .with_fill_rule(into_fill_rule(rule)); - if self.transforms.current.is_identity { + if self.transforms.current.is_identity() { self.fill_tessellator.tessellate_path( path.raw(), &options, buffer.as_mut(), ) } else { - let path = path.transform(&self.transforms.current.raw); + let path = path.transform(&self.transforms.current.0); self.fill_tessellator.tessellate_path( path.raw(), @@ -241,13 +251,14 @@ impl Frame { .buffers .get_fill(&self.transforms.current.transform_style(style)); - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); let size = - self.transforms.current.raw.transform_vector( + self.transforms.current.0.transform_vector( lyon::math::Vector::new(size.width, size.height), ); @@ -284,14 +295,14 @@ impl Frame { Cow::Owned(dashed(path, stroke.line_dash)) }; - if self.transforms.current.is_identity { + if self.transforms.current.is_identity() { self.stroke_tessellator.tessellate_path( path.raw(), &options, buffer.as_mut(), ) } else { - let path = path.transform(&self.transforms.current.raw); + let path = path.transform(&self.transforms.current.0); self.stroke_tessellator.tessellate_path( path.raw(), @@ -318,36 +329,57 @@ impl Frame { pub fn fill_text(&mut self, 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), - ); - - Point::new(transformed.x, transformed.y) - }; - - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; + let (scale_x, scale_y) = self.transforms.current.scale(); + + if self.transforms.current.is_scale_translation() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = + if self.transforms.current.is_identity() { + (text.position, text.size, text.line_height) + } else { + let position = + self.transforms.current.transform_point(text.position); + + let size = Pixels(text.size.0 * scale_y); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds, - color: text.color, - size: text.size, - line_height: text.line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }); + (position, size, line_height) + }; + + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }; + + // TODO: Honor layering! + self.primitives.push(Primitive::Text { + content: text.content, + bounds, + color: text.color, + size, + line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITY), + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } } /// Stores the current transform of the [`Frame`] and executes the given @@ -423,26 +455,24 @@ impl Frame { /// Applies a translation to the current transform of the [`Frame`]. #[inline] pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; + self.transforms.current.0 = + self.transforms + .current + .0 + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); } /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self + self.transforms.current.0 = self .transforms .current - .raw + .0 .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; } /// Applies a uniform scaling to the current transform of the [`Frame`]. @@ -458,9 +488,8 @@ impl Frame { pub fn scale_nonuniform(&mut self, scale: impl Into) { let scale = scale.into(); - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale.x, scale.y); - self.transforms.current.is_identity = false; + self.transforms.current.0 = + self.transforms.current.0.pre_scale(scale.x, scale.y); } /// Produces the [`Primitive`] representing everything drawn on the [`Frame`].