Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/assets/Arial.ttf
Binary file not shown.
24 changes: 24 additions & 0 deletions examples/ellipsis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use ril::prelude::*;

fn main() -> ril::Result<()> {
let font = Font::open("./examples/assets/Arial.ttf", 12.0)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Use a more portable way to specify font path

The hardcoded font path might not work on all systems. Consider using a system font or provide instructions on how to make the example work with different font locations to improve portability.

let font_path = std::env::var("FONT_PATH").unwrap_or_else(|_| "./examples/assets/Arial.ttf".to_string());
let font = Font::open(&font_path, 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(())
}
24 changes: 24 additions & 0 deletions examples/line_height.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
82 changes: 81 additions & 1 deletion src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>,
}

impl<'a, P: Pixel> TextSegment<'a, P> {
Expand All @@ -192,6 +194,7 @@ impl<'a, P: Pixel> TextSegment<'a, P> {
overlay: OverlayMode::Merge,
size: font.optimal_size(),
wrap: WrapStyle::Word,
max_height: None,
}
}

Expand Down Expand Up @@ -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`].
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Improve documentation for with_max_height method

The documentation for with_max_height should be more precise. Consider rewording it to clearly state that the ellipsization only occurs when used within a TextLayout.

Suggested change
/// Sets the maximum height of the text segment. If this is set, the text will be ellipsized if. It only works in a [`TextLayout`].
/// Sets the maximum height of the text segment.
/// When used within a [`TextLayout`], the text will be ellipsized if it exceeds this height.
/// Note: This setting only takes effect when the text is rendered 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 {
Expand Down Expand Up @@ -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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting the binary search logic into a separate function for improved modularity.

The truncate_text_with_ellipsis function introduces a clever binary search approach to efficiently handle text truncation with a height limit. While the algorithm is efficient, we can improve its readability and maintainability:

  1. Extract the binary search logic into a separate function:
fn find_truncation_point(text: &str, max_height: f32, font: &Font, size: f32, ellipsis: &str) -> usize {
    let mut left = 0;
    let mut right = text.len();

    while left + 1 < right {
        let mid = (left + right) / 2;
        let truncated = format!("{}{}", &text[..mid], ellipsis);

        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
        layout.reset(&self.settings);
        layout.append(&[font.inner()], &TextStyle::new(&truncated, size, 0));

        if layout.height() <= max_height {
            left = mid;
        } else {
            right = mid;
        }
    }
    left
}
  1. Simplify the main function using the extracted helper:
fn truncate_text_with_ellipsis(&self, segment: &TextSegment<'a, P>) -> String {
    let Some(max_height) = segment.max_height else {
        return segment.text.clone();
    };

    let ellipsis = "...";
    let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
    layout.reset(&self.settings);
    layout.append(&[segment.font.inner()], &TextStyle::new(&segment.text, segment.size, 0));

    if layout.height() <= max_height as f32 {
        return segment.text.clone();
    }

    let truncation_point = find_truncation_point(&segment.text, max_height as f32, segment.font, segment.size, ellipsis);

    if truncation_point == 0 {
        ellipsis.to_string()
    } else {
        let trimmed_text = segment.text[..truncation_point].trim_end();
        format!("{trimmed_text}{ellipsis}")
    }
}
  1. Add a comment explaining the binary search approach:
// We use a binary search to efficiently find the optimal truncation point.
// This approach is particularly effective for longer text strings, as it
// logarithmically reduces the number of layout calculations needed.
let truncation_point = find_truncation_point(&segment.text, max_height as f32, segment.font, segment.size, ellipsis);

These changes maintain the efficient binary search algorithm while improving code readability and maintainability. The extracted function can be easily unit tested, and the main function is now more focused and easier to understand.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Add comment explaining truncated_text usage

Consider adding a brief comment explaining why truncated_text is used instead of segment.text directly. This will help future maintainers quickly understand the reasoning behind this change.

// Truncate text if it exceeds layout bounds, adding ellipsis if needed
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),
Expand Down