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:
parent
12dda5fa1b
commit
938d93a64c
11 changed files with 238 additions and 66 deletions
|
@ -178,3 +178,7 @@ path = "examples/input.rs"
|
|||
[[example]]
|
||||
name = "shadow"
|
||||
path = "examples/shadow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "text_wrapper"
|
||||
path = "examples/text_wrapper.rs"
|
||||
|
|
59
crates/gpui/examples/text_wrapper.rs
Normal file
59
crates/gpui/examples/text_wrapper.rs
Normal 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();
|
||||
});
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -518,6 +518,7 @@ impl ConfigurationView {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
truncate: None,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key_editor,
|
||||
|
|
|
@ -403,6 +403,7 @@ impl ConfigurationView {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
truncate: None,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key_editor,
|
||||
|
|
|
@ -460,6 +460,7 @@ impl ConfigurationView {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
truncate: None,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key_editor,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue