Detailed changes
@@ -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 {}
@@ -20,6 +20,7 @@ impl Render for HelloWorld {
div()
.flex()
.flex_row()
+ .flex_shrink_0()
.gap_2()
.child(
div()
@@ -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))
@@ -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)),
@@ -89,5 +114,6 @@ fn main() {
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
+ cx.activate(true);
});
}
@@ -1677,6 +1677,7 @@ impl Interactivity {
FONT_SIZE,
&[window.text_style().to_run(str_len)],
None,
+ None,
)
.ok()
.and_then(|mut text| text.pop())
@@ -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 {
@@ -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
@@ -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 {
@@ -362,7 +361,8 @@ impl Default for TextStyle {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
- truncate: None,
+ text_overflow: None,
+ line_clamp: 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, 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,
@@ -11,6 +11,8 @@ pub use gpui_macros::{
};
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 {
@@ -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 {
@@ -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();
@@ -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,
@@ -129,9 +129,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.);
@@ -182,7 +182,15 @@ 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;
@@ -190,7 +198,6 @@ impl LineLayout {
last_boundary = boundary;
last_boundary_x = x;
}
-
boundaries.push(last_boundary);
}
prev_ch = ch;
@@ -434,6 +441,7 @@ impl LineLayoutCache {
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
+ max_lines: Option<usize>,
) -> Arc<WrappedLineLayout>
where
Text: AsRef<str>,
@@ -464,7 +472,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()
};
@@ -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;
}
@@ -564,6 +564,7 @@ mod tests {
normal.with_len(7),
],
Some(px(72.)),
+ None,
)
.unwrap();
@@ -647,11 +647,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,
@@ -466,7 +466,7 @@ impl ConfigurationView {
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
- truncate: None,
+ ..Default::default()
};
EditorElement::new(
&self.api_key_editor,
@@ -409,11 +409,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,
@@ -458,11 +458,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,
@@ -77,11 +77,9 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
line_height: window.line_height().into(),
background_color: Some(theme.colors().terminal_ansi_background),
white_space: WhiteSpace::Normal,
- truncate: None,
// These are going to be overridden per-cell
- underline: None,
- strikethrough: None,
color: theme.colors().terminal_foreground,
+ ..Default::default()
};
text_style
@@ -674,11 +674,9 @@ impl Element for TerminalElement {
line_height: line_height.into(),
background_color: Some(theme.colors().terminal_ansi_background),
white_space: WhiteSpace::Normal,
- truncate: None,
// These are going to be overridden per-cell
- underline: None,
- strikethrough: None,
color: theme.colors().terminal_foreground,
+ ..Default::default()
};
let text_system = cx.text_system();