From 401296ce2ab11bc163ff7075a0f39a8de171bc8c Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Wed, 19 Aug 2026 16:32:27 +0200 Subject: [PATCH] swr text layout: divide instead of counting lines one by one max_lines_that_fit added up line heights in a loop because TextShaper::Length had no length by length division: euclid's Length / Length gives a Scale, not a number. Add a small LengthDiv trait for that, with impls for f32, i16 and any euclid Length whose primitive implements it, and do a single division. --- internal/core/textlayout.rs | 10 ++----- internal/core/textlayout/shaping.rs | 44 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/internal/core/textlayout.rs b/internal/core/textlayout.rs index 0ca78da13f9..969dde79ce7 100644 --- a/internal/core/textlayout.rs +++ b/internal/core/textlayout.rs @@ -54,7 +54,7 @@ mod shaping; /// cbindgen:ignore pub mod sharedparley; use shaping::ShapeBuffer; -pub use shaping::{AbstractFont, FontMetrics, Glyph, TextShaper}; +pub use shaping::{AbstractFont, FontMetrics, Glyph, LengthDiv, TextShaper}; mod linebreaker; pub use linebreaker::TextLine; @@ -422,13 +422,7 @@ impl TextParagraphLayout<'_, Font> { if line_height <= Font::Length::zero() { return usize::MAX; } - let mut lines = 0usize; - let mut height = Font::Length::zero(); - while height + line_height <= self.max_height { - height += line_height; - lines += 1; - } - lines + self.max_height.div_count(line_height) } /// Returns the leading edge of the glyph at the given byte offset diff --git a/internal/core/textlayout/shaping.rs b/internal/core/textlayout/shaping.rs index 1583e81101b..fa854817275 100644 --- a/internal/core/textlayout/shaping.rs +++ b/internal/core/textlayout/shaping.rs @@ -57,6 +57,7 @@ pub trait TextShaper { + core::cmp::PartialOrd + core::ops::Mul + core::ops::Div + + LengthDiv + core::fmt::Debug; // Shapes the given string and emits the result into the given glyphs buffer. fn shape_text>>( @@ -67,6 +68,35 @@ pub trait TextShaper { fn glyph_for_char(&self, ch: char) -> Option>; } +/// How many times one length fits into another of the same unit. +/// euclid's `Length / Length` produces a `Scale` rather than a plain number, so the division the +/// paragraph layout needs is spelled out here instead of as a `core::ops::Div` bound. +pub trait LengthDiv { + /// The number of whole `divisor`s in `self`, truncated, and zero when that count is negative. + /// `divisor` must not be zero. + fn div_count(self, divisor: Self) -> usize; +} + +impl LengthDiv for f32 { + fn div_count(self, divisor: Self) -> usize { + // Casts from float saturate, so a negative or NaN ratio ends up at zero. + (self / divisor) as usize + } +} + +impl LengthDiv for i16 { + fn div_count(self, divisor: Self) -> usize { + // Widen so that i16::MIN / -1 can't overflow. + (i32::from(self) / i32::from(divisor)).max(0) as usize + } +} + +impl LengthDiv for euclid::Length { + fn div_count(self, divisor: Self) -> usize { + self.get().div_count(divisor.get()) + } +} + pub trait FontMetrics> { fn height(&self) -> Length { self.ascent() - self.descent() @@ -214,6 +244,20 @@ impl ShapeBuffer { } } +#[test] +fn test_div_count() { + assert_eq!(9.0_f32.div_count(3.0), 3); + assert_eq!(10.0_f32.div_count(3.0), 3); + assert_eq!((-10.0_f32).div_count(3.0), 0); + + type IntLen = euclid::Length; + assert_eq!(IntLen::new(10).div_count(IntLen::new(3)), 3); + assert_eq!(IntLen::new(-10).div_count(IntLen::new(3)), 0); + + type FloatLen = euclid::Length; + assert_eq!(FloatLen::new(10.).div_count(FloatLen::new(3.)), 3); +} + #[test] fn test_shape_boundaries_simple() { {