-
Notifications
You must be signed in to change notification settings - Fork 11
Adding text truncation logic to support ellipsis. #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7aa01de
5ada016
a0ae3da
f6b0b8b
d98cef1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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_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(()) | ||
| } | ||
| 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(()) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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> { | ||||||||||
|
|
@@ -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`]. | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||
| #[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 { | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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
}
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}")
}
}
// 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); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||
| 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), | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
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.