Skip to content

Commit

Permalink
gpui: Add line_clamp to support to truncate text after a specific …
Browse files Browse the repository at this point in the history
…number of lines.
  • Loading branch information
huacnlee committed Jan 26, 2025
1 parent 6fca1d2 commit 875f655
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 64 deletions.
40 changes: 33 additions & 7 deletions crates/gpui/examples/text_wrapper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use gpui::{
div, prelude::*, px, size, App, Application, Bounds, Context, Window, WindowBounds,
WindowOptions,
div, prelude::*, px, size, App, Application, Bounds, Context, TextOverflow, Window,
WindowBounds, WindowOptions,
};

struct HelloWorld {}
Expand All @@ -20,6 +20,7 @@ impl Render for HelloWorld {
div()
.flex()
.flex_row()
.flex_shrink_0()
.gap_2()
.child(
div()
Expand Down Expand Up @@ -49,29 +50,53 @@ impl Render for HelloWorld {
)
.child(
div()
.flex_shrink_0()
.text_xl()
.truncate()
.border_1()
.border_color(gpui::blue())
.child("ELLIPSIS: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_ellipsis()
.line_clamp(2)
.border_1()
.border_color(gpui::red())
.child("ELLIPSIS: ".to_owned() + text),
.border_color(gpui::blue())
.child("ELLIPSIS 2 lines: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.truncate()
.text_overflow(TextOverflow::Ellipsis(""))
.border_1()
.border_color(gpui::green())
.child("TRUNCATE: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_overflow(TextOverflow::Ellipsis(""))
.line_clamp(3)
.border_1()
.border_color(gpui::green())
.child("TRUNCATE 3 lines: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.whitespace_nowrap()
.overflow_hidden()
.border_1()
.border_color(gpui::blue())
.border_color(gpui::black())
.child("NOWRAP: ".to_owned() + text),
)
.child(div().text_xl().w_full().child(text))
Expand All @@ -80,7 +105,7 @@ impl Render for HelloWorld {

fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(600.0), px(480.0)), cx);
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
Expand All @@ -89,5 +114,6 @@ fn main() {
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
cx.activate(true);
});
}
1 change: 1 addition & 0 deletions crates/gpui/src/elements/div.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,7 @@ impl Interactivity {
FONT_SIZE,
&[window.text_style().to_run(str_len)],
None,
None,
)
.ok()
.and_then(|mut text| text.pop())
Expand Down
38 changes: 22 additions & 16 deletions crates/gpui/src/elements/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -255,8 +256,6 @@ struct TextLayoutInner {
bounds: Option<Bounds<Pixels>>,
}

const ELLIPSIS: &str = "…";

impl TextLayout {
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
self.0.lock()
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions crates/gpui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,10 @@ pub enum WhiteSpace {
}

/// How to truncate text that overflows the width of the element
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum Truncate {
/// Truncate the text without an ellipsis
#[default]
Truncate,
/// Truncate the text with an ellipsis
Ellipsis,
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextOverflow {
/// Truncate the text with an ellipsis, same as: `text-overflow: ellipsis;` in CSS
Ellipsis(&'static str),
}

/// The properties that can be used to style text in GPUI
Expand Down Expand Up @@ -337,7 +334,9 @@ pub struct TextStyle {
pub white_space: WhiteSpace,

/// The text should be truncated if it overflows the width of the element
pub truncate: Option<Truncate>,
pub text_overflow: Option<TextOverflow>,
/// The number of lines to display before truncating the text
pub line_clamp: Option<usize>,
}

impl Default for TextStyle {
Expand All @@ -362,7 +361,8 @@ impl Default for TextStyle {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
text_overflow: None,
line_clamp: None,
}
}
}
Expand Down
29 changes: 22 additions & 7 deletions crates/gpui/src/styled.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use crate::TextStyleRefinement;
use crate::{
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
SharedString, StrikethroughStyle, StyleRefinement, WhiteSpace,
SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, WhiteSpace,
};
use crate::{TextStyleRefinement, Truncate};
pub use gpui_macros::{
border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
overflow_style_methods, padding_style_methods, position_style_methods,
visibility_style_methods,
};
use taffy::style::{AlignContent, Display};

const ELLIPSIS: &str = "…";

/// A trait for elements that can be styled.
/// Use this to opt-in to a utility CSS-like styling API.
pub trait Styled: Sized {
Expand Down Expand Up @@ -64,19 +66,32 @@ pub trait Styled: Sized {
fn text_ellipsis(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.truncate = Some(Truncate::Ellipsis);
.text_overflow = Some(TextOverflow::Ellipsis(ELLIPSIS));
self
}

/// Sets the truncate overflowing text.
/// [Docs](https://tailwindcss.com/docs/text-overflow#truncate)
fn truncate(mut self) -> Self {
/// Sets the text overflow behavior of the element.
fn text_overflow(mut self, overflow: TextOverflow) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.truncate = Some(Truncate::Truncate);
.text_overflow = Some(overflow);
self
}

/// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis (…) if needed.
/// [Docs](https://tailwindcss.com/docs/text-overflow#truncate)
fn truncate(mut self) -> Self {
self.overflow_hidden().whitespace_nowrap().text_ellipsis()
}

/// Sets number of lines to show before truncating the text.
/// [Docs](https://tailwindcss.com/docs/line-clamp)
fn line_clamp(mut self, lines: usize) -> Self {
let mut text_style = self.text_style().get_or_insert_with(Default::default);
text_style.line_clamp = Some(lines);
self.overflow_hidden()
}

/// Sets the flex direction of the element to `column`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column)
fn flex_col(mut self) -> Self {
Expand Down
14 changes: 11 additions & 3 deletions crates/gpui/src/text_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,12 +374,15 @@ impl WindowTextSystem {
font_size: Pixels,
runs: &[TextRun],
wrap_width: Option<Pixels>,
line_clamp: Option<usize>,
) -> Result<SmallVec<[WrappedLine; 1]>> {
let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();

let mut lines = SmallVec::new();
let mut line_start = 0;
let mut max_wrap_lines = line_clamp.unwrap_or(usize::MAX);
let mut wrapped_lines = 0;

let mut process_line = |line_text: SharedString| {
let line_end = line_start + line_text.len();
Expand Down Expand Up @@ -430,9 +433,14 @@ impl WindowTextSystem {
run_start += run_len_within_line;
}

let layout = self
.line_layout_cache
.layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
let layout = self.line_layout_cache.layout_wrapped_line(
&line_text,
font_size,
&font_runs,
wrap_width,
Some(max_wrap_lines - wrapped_lines),
);
wrapped_lines += layout.wrap_boundaries.len();

lines.push(WrappedLine {
layout,
Expand Down
14 changes: 11 additions & 3 deletions crates/gpui/src/text_system/line_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ impl LineLayout {
&self,
text: &str,
wrap_width: Pixels,
max_lines: Option<usize>,
) -> SmallVec<[WrapBoundary; 1]> {
let mut boundaries = SmallVec::new();

let mut first_non_whitespace_ix = None;
let mut last_candidate_ix = None;
let mut last_candidate_x = px(0.);
Expand Down Expand Up @@ -174,15 +174,22 @@ impl LineLayout {

let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x);
let width = next_x - last_boundary_x;

if width > wrap_width && boundary > last_boundary {
// When used line_clamp, we should limit the number of lines.
if let Some(max_lines) = max_lines {
if boundaries.len() >= max_lines - 1 {
break;
}
}

if let Some(last_candidate_ix) = last_candidate_ix.take() {
last_boundary = last_candidate_ix;
last_boundary_x = last_candidate_x;
} else {
last_boundary = boundary;
last_boundary_x = x;
}

boundaries.push(last_boundary);
}
prev_ch = ch;
Expand Down Expand Up @@ -426,6 +433,7 @@ impl LineLayoutCache {
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
max_lines: Option<usize>,
) -> Arc<WrappedLineLayout>
where
Text: AsRef<str>,
Expand Down Expand Up @@ -456,7 +464,7 @@ impl LineLayoutCache {
let text = SharedString::from(text);
let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs);
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines)
} else {
SmallVec::new()
};
Expand Down
3 changes: 2 additions & 1 deletion crates/gpui/src/text_system/line_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ impl LineWrapper {
let mut char_indices = line.char_indices();
let mut truncate_ix = 0;
for (ix, c) in char_indices {
if width + ellipsis_width <= truncate_width {
if width + ellipsis_width < truncate_width {
truncate_ix = ix;
}

Expand Down Expand Up @@ -564,6 +564,7 @@ mod tests {
normal.with_len(7),
],
Some(px(72.)),
None,
)
.unwrap();

Expand Down
5 changes: 1 addition & 4 deletions crates/language_models/src/provider/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,11 +648,8 @@ impl ConfigurationView {
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
..Default::default()
};
EditorElement::new(
&self.api_key_editor,
Expand Down
5 changes: 1 addition & 4 deletions crates/language_models/src/provider/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,8 @@ impl ConfigurationView {
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
..Default::default()
};
EditorElement::new(
&self.api_key_editor,
Expand Down
Loading

0 comments on commit 875f655

Please sign in to comment.