gpui: Add truncate and text_ellipsis to TextStyle (#14850)

Release Notes:

- N/A

Ref issue #4996

## Demo

```
cargo run -p gpui --example text_wrapper 
```



https://github.com/user-attachments/assets/a7fcebf7-f287-4517-960d-76b12722a2d7

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Jason Lee 2024-08-24 02:02:51 +08:00 committed by GitHub
parent 12dda5fa1b
commit 938d93a64c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 238 additions and 66 deletions

View file

@ -178,3 +178,7 @@ path = "examples/input.rs"
[[example]]
name = "shadow"
path = "examples/shadow.rs"
[[example]]
name = "text_wrapper"
path = "examples/text_wrapper.rs"

View file

@ -0,0 +1,59 @@
use gpui::*;
struct HelloWorld {}
impl Render for HelloWorld {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let text = "The longest word in any of the major English language 以及中文的测试 dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that refers to a lung disease contracted from the inhalation of very fine silica particles, specifically from a volcano; medically, it is the same as silicosis.";
div()
.id("page")
.size_full()
.flex()
.flex_col()
.p_2()
.gap_2()
.bg(gpui::white())
.child(
div()
.text_xl()
.overflow_hidden()
.text_ellipsis()
.border_1()
.border_color(gpui::red())
.child("ELLIPSIS: ".to_owned() + text),
)
.child(
div()
.text_xl()
.overflow_hidden()
.truncate()
.border_1()
.border_color(gpui::green())
.child("TRUNCATE: ".to_owned() + text),
)
.child(
div()
.text_xl()
.whitespace_nowrap()
.overflow_hidden()
.border_1()
.border_color(gpui::blue())
.child("NOWRAP: ".to_owned() + text),
)
.child(div().text_xl().w_full().child(text))
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(600.0), px(480.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|_cx| HelloWorld {}),
)
.unwrap();
});
}

View file

@ -1,8 +1,8 @@
use crate::{
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
TOOLTIP_DELAY,
Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext,
WrappedLine, TOOLTIP_DELAY,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
@ -244,6 +244,8 @@ struct TextLayoutInner {
bounds: Option<Bounds<Pixels>>,
}
const ELLIPSIS: &str = "";
impl TextLayout {
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
self.0.lock()
@ -280,6 +282,20 @@ 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,
});
match truncate {
Truncate::Truncate => (width, None),
Truncate::Ellipsis => (width, Some(ELLIPSIS)),
}
} else {
(None, None)
};
if let Some(text_layout) = element_state.0.lock().as_ref() {
if text_layout.size.is_some()
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
@ -288,13 +304,17 @@ impl TextLayout {
}
}
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let text = if let Some(truncate_width) = truncate_width {
line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis)
} else {
text.clone()
};
let Some(lines) = cx
.text_system()
.shape_text(
text.clone(),
font_size,
&runs,
wrap_width, // Wrap if we know the width.
text, font_size, &runs, wrap_width, // Wrap if we know the width.
)
.log_err()
else {

View file

@ -282,6 +282,16 @@ pub enum WhiteSpace {
Nowrap,
}
/// 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,
}
/// The properties that can be used to style text in GPUI
#[derive(Refineable, Clone, Debug, PartialEq)]
#[refineable(Debug)]
@ -321,6 +331,9 @@ pub struct TextStyle {
/// How to handle whitespace in the text
pub white_space: WhiteSpace,
/// The text should be truncated if it overflows the width of the element
pub truncate: Option<Truncate>,
}
impl Default for TextStyle {
@ -345,6 +358,7 @@ impl Default for TextStyle {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
}
}
}

View file

@ -1,9 +1,9 @@
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, StyleRefinement, 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,
@ -59,6 +59,24 @@ pub trait Styled: Sized {
self
}
/// Sets the truncate overflowing text with an ellipsis (…) if needed.
/// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis)
fn text_ellipsis(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.truncate = Some(Truncate::Ellipsis);
self
}
/// Sets the truncate overflowing text.
/// [Docs](https://tailwindcss.com/docs/text-overflow#truncate)
fn truncate(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.truncate = Some(Truncate::Truncate);
self
}
/// Sets the flex direction of the element to `column`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column)
fn flex_col(mut self) -> Self {

View file

@ -1,4 +1,4 @@
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem};
use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString};
use collections::HashMap;
use std::{iter, sync::Arc};
@ -98,6 +98,32 @@ impl LineWrapper {
})
}
/// Truncate a line of text to the given width with this wrapper's font and font size.
pub fn truncate_line(
&mut self,
line: SharedString,
truncate_width: Pixels,
ellipsis: Option<&str>,
) -> SharedString {
let mut width = px(0.);
if let Some(ellipsis) = ellipsis {
for c in ellipsis.chars() {
width += self.width_for_char(c);
}
}
let mut char_indices = line.char_indices();
for (ix, c) in char_indices {
let char_width = self.width_for_char(c);
width += char_width;
if width > truncate_width {
return SharedString::from(format!("{}{}", &line[..ix], ellipsis.unwrap_or("")));
}
}
line.clone()
}
pub(crate) fn is_word_char(c: char) -> bool {
// ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
c.is_ascii_alphanumeric() ||
@ -181,8 +207,7 @@ mod tests {
use crate::{TextRun, WindowTextSystem, WrapBoundary};
use rand::prelude::*;
#[test]
fn test_wrap_line() {
fn build_wrapper() -> LineWrapper {
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
let cx = TestAppContext::new(dispatcher, None);
cx.text_system()
@ -193,63 +218,90 @@ mod tests {
.into()])
.unwrap();
let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap();
LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
}
cx.update(|cx| {
let text_system = cx.text_system().clone();
let mut wrapper =
LineWrapper::new(id, px(16.), text_system.platform_text_system.clone());
assert_eq!(
wrapper
.wrap_line("aa bbb cccc ddddd eeee", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper
.wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
.collect::<Vec<_>>(),
&[
Boundary::new(4, 0),
Boundary::new(11, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaa", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 5),
Boundary::new(9, 5),
Boundary::new(11, 5),
]
);
assert_eq!(
wrapper
.wrap_line(" ", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 0),
Boundary::new(21, 0)
]
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaaaaaaaaa", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 3),
Boundary::new(18, 3),
Boundary::new(22, 3),
]
);
});
#[test]
fn test_wrap_line() {
let mut wrapper = build_wrapper();
assert_eq!(
wrapper
.wrap_line("aa bbb cccc ddddd eeee", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper
.wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
.collect::<Vec<_>>(),
&[
Boundary::new(4, 0),
Boundary::new(11, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaa", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 5),
Boundary::new(9, 5),
Boundary::new(11, 5),
]
);
assert_eq!(
wrapper
.wrap_line(" ", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 0),
Boundary::new(21, 0)
]
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaaaaaaaaa", px(72.))
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 3),
Boundary::new(18, 3),
Boundary::new(22, 3),
]
);
}
#[test]
fn test_truncate_line() {
let mut wrapper = build_wrapper();
assert_eq!(
wrapper.truncate_line("aa bbb cccc ddddd eeee ffff gggg".into(), px(220.), None),
"aa bbb cccc ddddd eeee"
);
assert_eq!(
wrapper.truncate_line(
"aa bbb cccc ddddd eeee ffff gggg".into(),
px(220.),
Some("")
),
"aa bbb cccc ddddd eee…"
);
assert_eq!(
wrapper.truncate_line(
"aa bbb cccc ddddd eeee ffff gggg".into(),
px(220.),
Some("......")
),
"aa bbb cccc dddd......"
);
}
#[test]

View file

@ -518,6 +518,7 @@ impl ConfigurationView {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
};
EditorElement::new(
&self.api_key_editor,

View file

@ -403,6 +403,7 @@ impl ConfigurationView {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
};
EditorElement::new(
&self.api_key_editor,

View file

@ -460,6 +460,7 @@ impl ConfigurationView {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
truncate: None,
};
EditorElement::new(
&self.api_key_editor,

View file

@ -45,6 +45,7 @@ pub fn text_style(cx: &mut WindowContext) -> TextStyle {
line_height: cx.line_height().into(),
background_color: Some(theme.colors().terminal_background),
white_space: WhiteSpace::Normal,
truncate: None,
// These are going to be overridden per-cell
underline: None,
strikethrough: None,

View file

@ -667,6 +667,7 @@ impl Element for TerminalElement {
line_height: line_height.into(),
background_color: Some(theme.colors().terminal_background),
white_space: WhiteSpace::Normal,
truncate: None,
// These are going to be overridden per-cell
underline: None,
strikethrough: None,