diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 24d8212c..a39ebbcd 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -179,9 +179,10 @@ fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> { size: Pixels::from(16), line_height: text::LineHeight::default(), font: Font::DEFAULT, - align_x: alignment::Horizontal::Left, + align_x: text::Alignment::Left, align_y: alignment::Vertical::Top, shaping: text::Shaping::Basic, + max_width: f32::INFINITY, }); } })] diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index b7811653..89855bc2 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -2,7 +2,7 @@ use iced::alignment; use iced::mouse; use iced::time::{self, milliseconds}; use iced::widget::canvas::{Cache, Geometry, LineCap, Path, Stroke, stroke}; -use iced::widget::{canvas, container}; +use iced::widget::{canvas, container, text}; use iced::{ Degrees, Element, Fill, Font, Point, Radians, Rectangle, Renderer, Size, Subscription, Theme, Vector, @@ -148,9 +148,9 @@ impl canvas::Program for Clock { ), color: palette.secondary.strong.text, align_x: if rotate_factor > 0.0 { - alignment::Horizontal::Right + text::Alignment::Right } else { - alignment::Horizontal::Left + text::Alignment::Left }, align_y: alignment::Vertical::Bottom, font: Font::MONOSPACE, @@ -170,7 +170,7 @@ impl canvas::Program for Clock { size: (radius / 5.0).into(), position: Point::new(x * 0.82, y * 0.82), color: palette.secondary.strong.text, - align_x: alignment::Horizontal::Center, + align_x: text::Alignment::Center, align_y: alignment::Vertical::Center, font: Font::MONOSPACE, ..canvas::Text::default() diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index e705c40d..10e72d3b 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -169,7 +169,7 @@ impl Theme { }); let mut text = canvas::Text { - align_x: alignment::Horizontal::Center, + align_x: text::Alignment::Center, align_y: alignment::Vertical::Top, size: Pixels(15.0), color: text_color, diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8e0ecc45..8860759e 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -187,6 +187,7 @@ mod grid { use iced::widget::canvas::{ Cache, Canvas, Event, Frame, Geometry, Path, Text, }; + use iced::widget::text; use iced::{ Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; @@ -575,7 +576,7 @@ mod grid { color: Color::WHITE, size: 14.0.into(), position: Point::new(frame.width(), frame.height()), - align_x: alignment::Horizontal::Right, + align_x: text::Alignment::Right, align_y: alignment::Vertical::Bottom, ..Text::default() }; diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 92eacca8..72525d55 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -141,7 +141,7 @@ impl canvas::Program for State { } else { "Vectorial Text! 🎉" }), - align_x: alignment::Horizontal::Center, + align_x: text::Alignment::Center, align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, ..canvas::Text::default() diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index baab7e0f..a1c3d7fa 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,5 +1,6 @@ +use crate::core; use crate::core::alignment; -use crate::core::text::{LineHeight, Shaping}; +use crate::core::text::{Alignment, LineHeight, Paragraph, Shaping, Wrapping}; use crate::core::{Color, Font, Pixels, Point, Size, Vector}; use crate::geometry::Path; use crate::text; @@ -21,6 +22,10 @@ pub struct Text { /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the /// center of the text will be placed at the given position NOT the top-left coordinate. pub position: Point, + /// The maximum horizontal space available for this [`Text`]. + /// + /// Text will break into new lines when the width is reached. + pub max_width: f32, /// The color of the text pub color: Color, /// The size of the text @@ -30,7 +35,7 @@ pub struct Text { /// The font of the text pub font: Font, /// The horizontal alignment of the text - pub align_x: alignment::Horizontal, + pub align_x: Alignment, /// The vertical alignment of the text pub align_y: alignment::Vertical, /// The shaping strategy of the text. @@ -41,62 +46,50 @@ 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::LineEnding::default(), - cosmic_text::AttrsList::new(&text::to_attributes(self.font)), - text::to_shaping(self.shaping), - ); - - let layout = buffer.layout( - font_system.raw(), - self.size.0, - None, - cosmic_text::Wrap::None, - None, - 4, - ); + let paragraph = text::Paragraph::with_text(core::text::Text { + content: &self.content, + bounds: Size::new(self.max_width, f32::INFINITY), + size: self.size, + line_height: self.line_height, + font: self.font, + align_x: self.align_x, + align_y: self.align_y, + shaping: self.shaping, + wrapping: Wrapping::default(), + }); let translation_x = match self.align_x { - 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.align_x == alignment::Horizontal::Center { - self.position.x - line_width / 2.0 - } else { - self.position.x - line_width - } + Alignment::Default | Alignment::Left | Alignment::Justified => { + self.position.x } + Alignment::Center => self.position.x - paragraph.min_width() / 2.0, + Alignment::Right => self.position.x - paragraph.min_width(), }; let translation_y = { - let line_height = self.line_height.to_absolute(self.size); - match self.align_y { alignment::Vertical::Top => self.position.y, alignment::Vertical::Center => { - self.position.y - line_height.0 / 2.0 + self.position.y - paragraph.min_height() / 2.0 + } + alignment::Vertical::Bottom => { + self.position.y - paragraph.min_height() } - alignment::Vertical::Bottom => self.position.y - line_height.0, } }; + let buffer = paragraph.buffer(); let mut swash_cache = cosmic_text::SwashCache::new(); - for run in layout.iter() { + let mut font_system = + text::font_system().write().expect("Write font system"); + + for run in buffer.layout_runs() { 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 start_y = translation_y + glyph.y_offset + run.line_y; let offset = Vector::new(start_x, start_y); if let Some(commands) = swash_cache.get_outline_commands( @@ -176,11 +169,12 @@ impl Default for Text { Text { content: String::new(), position: Point::ORIGIN, + max_width: f32::INFINITY, color: Color::BLACK, size: Pixels(16.0), line_height: LineHeight::Relative(1.2), font: Font::default(), - align_x: alignment::Horizontal::Left, + align_x: Alignment::Default, align_y: alignment::Vertical::Top, shaping: Shaping::Basic, } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 507ef178..14262076 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -257,6 +257,47 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> (Size, bool) { (Size::new(width, height), has_rtl) } +/// Aligns the given [`cosmic_text::Buffer`] with the given [`Alignment`] +/// and returns its minimum [`Size`]. +pub fn align( + buffer: &mut cosmic_text::Buffer, + font_system: &mut cosmic_text::FontSystem, + alignment: Alignment, +) -> Size { + let (min_bounds, has_rtl) = measure(buffer); + let mut needs_relayout = has_rtl; + + if let Some(align) = to_align(alignment) { + let has_multiple_lines = buffer.lines.len() > 1 + || buffer.lines.first().is_some_and(|line| { + line.layout_opt().is_some_and(|layout| layout.len() > 1) + }); + + if has_multiple_lines { + for line in &mut buffer.lines { + let _ = line.set_align(Some(align)); + } + + needs_relayout = true; + } else if let Some(line) = buffer.lines.first_mut() { + needs_relayout = line.set_align(None); + } + } + + // TODO: Avoid relayout with some changes to `cosmic-text` (?) + if needs_relayout { + log::trace!("Relayouting paragraph..."); + + buffer.set_size( + font_system, + Some(min_bounds.width), + Some(min_bounds.height), + ); + } + + min_bounds +} + /// Returns the attributes of the given [`Font`]. pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { cosmic_text::Attrs::new() diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 5de2a2fb..9bb66362 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -58,15 +58,7 @@ impl Cache { text::to_shaping(key.shaping), ); - let (bounds, has_rtl) = text::measure(&buffer); - - if has_rtl { - buffer.set_size( - font_system, - Some(bounds.width), - Some(bounds.height), - ); - } + let bounds = text::align(&mut buffer, font_system, key.align_x); let _ = entry.insert(Entry { buffer, @@ -123,6 +115,8 @@ pub struct Key<'a> { pub bounds: Size, /// The shaping strategy of the text. pub shaping: text::Shaping, + /// The alignment of the text. + pub align_x: text::Alignment, } impl Key<'_> { @@ -134,6 +128,7 @@ impl Key<'_> { self.bounds.width.to_bits().hash(&mut hasher); self.bounds.height.to_bits().hash(&mut hasher); self.shaping.hash(&mut hasher); + self.align_x.hash(&mut hasher); hasher.finish() } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 9cda9cb0..6b8e7167 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -89,7 +89,8 @@ impl core::text::Paragraph for Paragraph { text::to_shaping(text.shaping), ); - let min_bounds = align(&mut buffer, &mut font_system, text.align_x); + let min_bounds = + text::align(&mut buffer, font_system.raw(), text.align_x); Self(Arc::new(Internal { buffer, @@ -159,7 +160,8 @@ impl core::text::Paragraph for Paragraph { None, ); - let min_bounds = align(&mut buffer, &mut font_system, text.align_x); + let min_bounds = + text::align(&mut buffer, font_system.raw(), text.align_x); Self(Arc::new(Internal { buffer, @@ -186,8 +188,11 @@ impl core::text::Paragraph for Paragraph { Some(new_bounds.height), ); - let min_bounds = - align(&mut paragraph.buffer, &mut font_system, paragraph.align_x); + let min_bounds = text::align( + &mut paragraph.buffer, + font_system.raw(), + paragraph.align_x, + ); paragraph.bounds = new_bounds; paragraph.min_bounds = min_bounds; @@ -357,45 +362,6 @@ impl core::text::Paragraph for Paragraph { } } -fn align( - buffer: &mut cosmic_text::Buffer, - font_system: &mut text::FontSystem, - alignment: Alignment, -) -> Size { - let (min_bounds, has_rtl) = text::measure(buffer); - let mut needs_relayout = has_rtl; - - if let Some(align) = text::to_align(alignment) { - let has_multiple_lines = buffer.lines.len() > 1 - || buffer.lines.first().is_some_and(|line| { - line.layout_opt().is_some_and(|layout| layout.len() > 1) - }); - - if has_multiple_lines { - for line in &mut buffer.lines { - let _ = line.set_align(Some(align)); - } - - needs_relayout = true; - } else if let Some(line) = buffer.lines.first_mut() { - needs_relayout = line.set_align(None); - } - } - - // TODO: Avoid relayout with some changes to `cosmic-text` (?) - if needs_relayout { - log::trace!("Relayouting paragraph..."); - - buffer.set_size( - font_system.raw(), - Some(min_bounds.width), - Some(min_bounds.height), - ); - } - - min_bounds -} - impl Default for Paragraph { fn default() -> Self { Self(Arc::new(Internal::default())) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index e089bbb1..11367b7c 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -187,9 +187,15 @@ impl geometry::frame::Backend for Frame { && 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) + let (bounds, size, line_height) = if self.transform.is_identity() { + ( + Rectangle::new( + text.position, + Size::new(text.max_width, f32::INFINITY), + ), + text.size, + text.line_height, + ) } else { let mut position = [tiny_skia::Point { x: text.position.x, @@ -210,19 +216,17 @@ impl geometry::frame::Backend for Frame { }; ( - Point::new(position[0].x, position[0].y), + Rectangle { + x: position[0].x, + y: position[0].y, + width: text.max_width * scale_x, + height: f32::INFINITY, + }, size.into(), line_height, ) }; - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - // TODO: Honor layering! self.text.push(Text::Cached { content: text.content, @@ -231,7 +235,7 @@ impl geometry::frame::Backend for Frame { size, line_height: line_height.to_absolute(size), font: text.font, - align_x: text.align_x.into(), + align_x: text.align_x, align_y: text.align_y, shaping: text.shaping, clip_bounds: Rectangle::with_size(Size::INFINITY), diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 9f55458d..1ef825ae 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -108,8 +108,8 @@ impl Pipeline { size: Pixels, line_height: Pixels, font: Font, - horizontal_alignment: Alignment, - vertical_alignment: alignment::Vertical, + align_x: Alignment, + align_y: alignment::Vertical, shaping: Shaping, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, @@ -127,6 +127,7 @@ impl Pipeline { size: size.into(), line_height, shaping, + align_x, }; let (_, entry) = self.cache.get_mut().allocate(font_system, key); @@ -144,8 +145,8 @@ impl Pipeline { ..bounds }, color, - horizontal_alignment, - vertical_alignment, + align_x, + align_y, pixels, clip_mask, transformation, diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 881eb6be..da98e479 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -301,9 +301,16 @@ impl geometry::frame::Backend for Frame { && scale_x > 0.0 && scale_y > 0.0 { - let (position, size, line_height) = + let (bounds, size, line_height) = if self.transforms.current.is_identity() { - (text.position, text.size, text.line_height) + ( + Rectangle::new( + text.position, + Size::new(text.max_width, f32::INFINITY), + ), + text.size, + text.line_height, + ) } else { let position = self.transforms.current.transform_point(text.position); @@ -319,16 +326,16 @@ impl geometry::frame::Backend for Frame { } }; - (position, size, line_height) + ( + Rectangle::new( + position, + Size::new(text.max_width, f32::INFINITY), + ), + size, + line_height, + ) }; - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - self.text.push(Text::Cached { content: text.content, bounds, @@ -336,7 +343,7 @@ impl geometry::frame::Backend for Frame { size, line_height: line_height.to_absolute(size), font: text.font, - align_x: text.align_x.into(), + align_x: text.align_x, align_y: text.align_y, shaping: text.shaping, clip_bounds: self.clip_bounds, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2a4c6161..b3b2ee68 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -479,6 +479,7 @@ fn prepare( line_height, font, shaping, + align_x, .. } => { let (key, _) = buffer_cache.allocate( @@ -488,6 +489,7 @@ fn prepare( size: f32::from(*size), line_height: f32::from(*line_height), font: *font, + align_x: *align_x, bounds: Size { width: bounds.width, height: bounds.height,