diff --git a/examples/assets/Arial.ttf b/examples/assets/Arial.ttf new file mode 100644 index 00000000..ab68fb19 Binary files /dev/null and b/examples/assets/Arial.ttf differ diff --git a/examples/ellipsis.rs b/examples/ellipsis.rs new file mode 100644 index 00000000..42b05241 --- /dev/null +++ b/examples/ellipsis.rs @@ -0,0 +1,24 @@ +use ril::prelude::*; + +fn main() -> ril::Result<()> { + let font = Font::open("./examples/assets/Arial.ttf", 12.0)?; + + let mut image = Image::new(72, 72, Rgba::black()); + + let (x, y) = image.center(); + let layout = TextLayout::new() + .centered() // Shorthand for centering horizontally and vertically + .with_wrap(WrapStyle::Word) // RIL supports word wrapping + .with_width(image.width()) // This is the width to wrap text at. Only required if you want to wrap text. + .with_position(x, y) // Position the anchor (which is the center) at the center of the image + .with_segment( + &TextSegment::new(&font, "Here is some multi line text.", Rgba::white()) + .with_max_height(30), + ); + + image.draw(&layout); + + image.save_inferred("sample_ellipsis.jpg")?; + + Ok(()) +} diff --git a/examples/line_height.rs b/examples/line_height.rs new file mode 100644 index 00000000..6aac243c --- /dev/null +++ b/examples/line_height.rs @@ -0,0 +1,24 @@ +use ril::prelude::*; + +fn main() -> ril::Result<()> { + let font = Font::open("./examples/assets/Arial.ttf", 12.0)?; + + let mut image = Image::new(72, 72, Rgba::black()); + + let (x, y) = image.center(); + let layout = TextLayout::new() + .centered() // Shorthand for centering horizontally and vertically + .with_wrap(WrapStyle::Word) // RIL supports word wrapping + .with_width(image.width()) // This is the width to wrap text at. Only required if you want to wrap text. + .with_position(x, y) // Position the anchor (which is the center) at the center of the image + .with_line_height(0.8) // This is the line height. It's the distance between the baselines of two lines of text. + .with_segment(&TextSegment::new(&font, "Here is some ", Rgba::white())) + .with_segment(&TextSegment::new(&font, "multi line ", Rgba::white())) + .with_segment(&TextSegment::new(&font, "text.", Rgba::white())); + + image.draw(&layout); + + image.save_inferred("sample_line_height.jpg")?; + + Ok(()) +} diff --git a/src/text.rs b/src/text.rs index 54c62822..f7111d8f 100644 --- a/src/text.rs +++ b/src/text.rs @@ -169,6 +169,8 @@ pub struct TextSegment<'a, P: Pixel> { /// If this is used in a [`TextLayout`], this is ignored and [`TextLayout::with_wrap`] is /// used instead. pub wrap: WrapStyle, + /// The maximum height of the text. If this is set, the text will be ellipsized if it exceeds + pub max_height: Option, } impl<'a, P: Pixel> TextSegment<'a, P> { @@ -192,6 +194,7 @@ impl<'a, P: Pixel> TextSegment<'a, P> { overlay: OverlayMode::Merge, size: font.optimal_size(), wrap: WrapStyle::Word, + max_height: None, } } @@ -232,6 +235,13 @@ impl<'a, P: Pixel> TextSegment<'a, P> { self } + /// Sets the maximum height of the text segment. If this is set, the text will be ellipsized if. It only works in a [`TextLayout`]. + #[must_use] + pub const fn with_max_height(mut self, height: u32) -> Self { + self.max_height = Some(height); + self + } + fn layout(&self) -> Layout<(P, OverlayMode)> { let mut layout = Layout::new(CoordinateSystem::PositiveYDown); layout.reset(&LayoutSettings { @@ -495,13 +505,83 @@ impl<'a, P: Pixel> TextLayout<'a, P> { self } + /// Sets the line height of the text. + /// + /// **This must be set before adding any text segments!** + #[must_use] + pub fn with_line_height(mut self, line_height: f32) -> Self { + self.set_settings(LayoutSettings { + line_height, + ..self.settings + }); + self + } + + fn truncate_text_with_ellipsis(&self, segment: &TextSegment<'a, P>) -> String { + if segment.max_height.is_none() { + return segment.text.clone(); + } + + let max_height = segment.max_height.unwrap() as f32; + let ellipsis = "..."; + + // Create a new layout with the same settings as the main layout + let mut layout = Layout::new(CoordinateSystem::PositiveYDown); + layout.reset(&self.settings); + + // Check if the entire text fits within the max height + layout.append( + &[segment.font.inner()], + &TextStyle::new(&segment.text, segment.size, 0), + ); + + if layout.height() <= max_height { + return segment.text.clone(); + } + + // Binary search to find the optimal truncation point + let mut left = 0; + let mut right = segment.text.len(); + + while left + 1 < right { + let mid = (left + right) / 2; + // Create truncated text with ellipsis + let truncated = format!("{}{}", &segment.text[..mid], ellipsis); + + // Clear the layout and append the truncated text + layout.clear(); + layout.append( + &[segment.font.inner()], + &TextStyle::new(&truncated, segment.size, 0), + ); + + // If the truncated text fits, try a longer string; otherwise, try a shorter one + if layout.height() <= max_height { + left = mid; + } else { + right = mid; + } + } + + if left == 0 { + // If even the shortest truncation doesn't fit, just return the ellipsis + ellipsis.to_string() + } else { + // Trim whitespace at the end before adding ellipsis + let trimmed_text = segment.text[..left].trim_end(); + format!("{trimmed_text}{ellipsis}") + } + } + + /// Adds a text segment to the text layout. pub fn push_segment(&mut self, segment: &TextSegment<'a, P>) { + let truncated_text = self.truncate_text_with_ellipsis(segment); self.fonts.push(segment.font.inner()); self.inner.append( &self.fonts, &TextStyle::with_user_data( - &segment.text, + &truncated_text, segment.size, self.fonts.len() - 1, (segment.fill, segment.overlay),