gpui: Add line_clamp to truncate text after a specified number of lines (#23058)

Release Notes:

- N/A

Add this feature for some case we need keep 2 or 3 lines, but truncate.
For example the blog post summary.

- Added `line_clamp` method.
    Ref: https://tailwindcss.com/docs/line-clamp


## Break changes:

- Renamed `gpui::Truncate` to `gpui::TextOverflow` to match
[CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow).
- Update `truncate` style method to match [Tailwind
CSS](https://tailwindcss.com/docs/text-overflow) behavior:

    ```css
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    ```
<img width="538" alt="image"
src="https://github.com/user-attachments/assets/c69c4213-eac9-4087-9daa-ce7afe18c758"
/>


## Show case

<img width="816" alt="image"
src="https://github.com/user-attachments/assets/e0660290-8042-4954-b93c-c729d609484a"
/>

![CleanShot 2025-01-13 at 17 22
05](https://github.com/user-attachments/assets/38644892-79fe-4254-af9e-88c1349561bd)

## Describe changes

The [second
commit](https://github.com/zed-industries/zed/commit/6b41c2772f9d1c5f4efe69fe83206a14407af4d4)
for make sure text layout to match with the line clamp. Before this
change, they may wrap many lines in sometimes. And I also make
line_clamp default to 1 if we used `truncate` to ensure no wrap.

> TODO: There is still a tiny detail that is not easy to fix. This
problem only occurs in the case of certain long words. I will think
about how to improve it later. At present, this has some flaws but does
not affect the use.
This commit is contained in:
Jason Lee
2025-01-29 22:14:24 +02:00
committed by GitHub
parent baac01cea4
commit 706f7be5e7
14 changed files with 118 additions and 66 deletions
+1
View File
@@ -1677,6 +1677,7 @@ impl Interactivity {
FONT_SIZE,
&[window.text_style().to_run(str_len)],
None,
None,
)
.ok()
.and_then(|mut text| text.pop())
+22 -16
View File
@@ -2,7 +2,8 @@ use crate::{
register_tooltip_mouse_handlers, set_tooltip_on_window, ActiveTooltip, AnyView, App, Bounds,
DispatchPhase, Element, ElementId, GlobalElementId, HighlightStyle, Hitbox, IntoElement,
LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size,
TextRun, TextStyle, TooltipId, Truncate, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
TextOverflow, TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine,
WrappedLineLayout,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
@@ -255,8 +256,6 @@ struct TextLayoutInner {
bounds: Option<Bounds<Pixels>>,
}
const ELLIPSIS: &str = "…";
impl TextLayout {
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
self.0.lock()
@@ -294,19 +293,22 @@ impl TextLayout {
None
};
let (truncate_width, ellipsis) = if let Some(truncate) = text_style.truncate {
let width = known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => Some(x),
_ => None,
});
let (truncate_width, ellipsis) =
if let Some(text_overflow) = text_style.text_overflow {
let width = known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
Some(max_lines) => Some(x * max_lines),
None => Some(x),
},
_ => None,
});
match truncate {
Truncate::Truncate => (width, None),
Truncate::Ellipsis => (width, Some(ELLIPSIS)),
}
} else {
(None, None)
};
match text_overflow {
TextOverflow::Ellipsis(s) => (width, Some(s)),
}
} else {
(None, None)
};
if let Some(text_layout) = element_state.0.lock().as_ref() {
if text_layout.size.is_some()
@@ -326,7 +328,11 @@ impl TextLayout {
let Some(lines) = window
.text_system()
.shape_text(
text, font_size, &runs, wrap_width, // Wrap if we know the width.
text,
font_size,
&runs,
wrap_width, // Wrap if we know the width.
text_style.line_clamp, // Limit the number of lines if line_clamp is set.
)
.log_err()
else {