Detailed changes
@@ -354,7 +354,7 @@ impl TextLayout {
None
};
- let (truncate_width, truncation_suffix) =
+ let (truncate_width, truncation_suffix, truncate_start) =
if let Some(text_overflow) = text_style.text_overflow.clone() {
let width = known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
@@ -365,10 +365,11 @@ impl TextLayout {
});
match text_overflow {
- TextOverflow::Truncate(s) => (width, s),
+ TextOverflow::Truncate(s) => (width, s, false),
+ TextOverflow::TruncateStart(s) => (width, s, true),
}
} else {
- (None, "".into())
+ (None, "".into(), false)
};
if let Some(text_layout) = element_state.0.borrow().as_ref()
@@ -380,12 +381,21 @@ impl TextLayout {
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let (text, runs) = if let Some(truncate_width) = truncate_width {
- line_wrapper.truncate_line(
- text.clone(),
- truncate_width,
- &truncation_suffix,
- &runs,
- )
+ if truncate_start {
+ line_wrapper.truncate_line_start(
+ text.clone(),
+ truncate_width,
+ &truncation_suffix,
+ &runs,
+ )
+ } else {
+ line_wrapper.truncate_line(
+ text.clone(),
+ truncate_width,
+ &truncation_suffix,
+ &runs,
+ )
+ }
} else {
(text.clone(), Cow::Borrowed(&*runs))
};
@@ -334,9 +334,13 @@ pub enum WhiteSpace {
/// How to truncate text that overflows the width of the element
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TextOverflow {
- /// Truncate the text when it doesn't fit, and represent this truncation by displaying the
- /// provided string.
+ /// Truncate the text at the end when it doesn't fit, and represent this truncation by
+ /// displaying the provided string (e.g., "very long teβ¦").
Truncate(SharedString),
+ /// Truncate the text at the start when it doesn't fit, and represent this truncation by
+ /// displaying the provided string at the beginning (e.g., "β¦ong text here").
+ /// Typically more adequate for file paths where the end is more important than the beginning.
+ TruncateStart(SharedString),
}
/// How to align text within the element
@@ -75,13 +75,21 @@ pub trait Styled: Sized {
self
}
- /// Sets the truncate overflowing text with an ellipsis (β¦) if needed.
+ /// Sets the truncate overflowing text with an ellipsis (β¦) at the end if needed.
/// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis)
fn text_ellipsis(mut self) -> Self {
self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS));
self
}
+ /// Sets the truncate overflowing text with an ellipsis (β¦) at the start if needed.
+ /// Typically more adequate for file paths where the end is more important than the beginning.
+ /// Note: This doesn't exist in Tailwind CSS.
+ fn text_ellipsis_start(mut self) -> Self {
+ self.text_style().text_overflow = Some(TextOverflow::TruncateStart(ELLIPSIS));
+ self
+ }
+
/// Sets the text overflow behavior of the element.
fn text_overflow(mut self, overflow: TextOverflow) -> Self {
self.text_style().text_overflow = Some(overflow);
@@ -129,6 +129,7 @@ impl LineWrapper {
}
/// Truncate a line of text to the given width with this wrapper's font and font size.
+ /// Truncates from the end, e.g., "very long teβ¦"
pub fn truncate_line<'a>(
&mut self,
line: SharedString,
@@ -164,6 +165,65 @@ impl LineWrapper {
(line, Cow::Borrowed(runs))
}
+ /// Truncate a line of text from the start to the given width.
+ /// Truncates from the beginning, e.g., "β¦ong text here"
+ pub fn truncate_line_start<'a>(
+ &mut self,
+ line: SharedString,
+ truncate_width: Pixels,
+ truncation_prefix: &str,
+ runs: &'a [TextRun],
+ ) -> (SharedString, Cow<'a, [TextRun]>) {
+ // First, measure the full line width to see if truncation is needed
+ let full_width: Pixels = line.chars().map(|c| self.width_for_char(c)).sum();
+
+ if full_width <= truncate_width {
+ return (line, Cow::Borrowed(runs));
+ }
+
+ let prefix_width: Pixels = truncation_prefix
+ .chars()
+ .map(|c| self.width_for_char(c))
+ .sum();
+
+ let available_width = truncate_width - prefix_width;
+
+ if available_width <= px(0.) {
+ return (
+ SharedString::from(truncation_prefix.to_string()),
+ Cow::Owned(vec![]),
+ );
+ }
+
+ // Work backwards from the end to find where to start the visible text
+ let char_indices: Vec<(usize, char)> = line.char_indices().collect();
+ let mut width_from_end = px(0.);
+ let mut start_byte_index = line.len();
+
+ for (byte_index, c) in char_indices.iter().rev() {
+ let char_width = self.width_for_char(*c);
+ if width_from_end + char_width > available_width {
+ break;
+ }
+ width_from_end += char_width;
+ start_byte_index = *byte_index;
+ }
+
+ if start_byte_index == 0 {
+ return (line, Cow::Borrowed(runs));
+ }
+
+ let result = SharedString::from(format!(
+ "{}{}",
+ truncation_prefix,
+ &line[start_byte_index..]
+ ));
+ let mut runs = runs.to_vec();
+ update_runs_after_start_truncation(&result, truncation_prefix, start_byte_index, &mut runs);
+
+ (result, Cow::Owned(runs))
+ }
+
/// Any character in this list should be treated as a word character,
/// meaning it can be part of a word that should not be wrapped.
pub(crate) fn is_word_char(c: char) -> bool {
@@ -238,6 +298,52 @@ fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<Tex
}
}
+fn update_runs_after_start_truncation(
+ result: &str,
+ prefix: &str,
+ bytes_removed: usize,
+ runs: &mut Vec<TextRun>,
+) {
+ let prefix_len = prefix.len();
+
+ let mut bytes_to_skip = bytes_removed;
+ let mut first_relevant_run = 0;
+
+ for (index, run) in runs.iter().enumerate() {
+ if bytes_to_skip >= run.len {
+ bytes_to_skip -= run.len;
+ first_relevant_run = index + 1;
+ } else {
+ break;
+ }
+ }
+
+ if first_relevant_run > 0 {
+ runs.drain(0..first_relevant_run);
+ }
+
+ if !runs.is_empty() && bytes_to_skip > 0 {
+ runs[0].len -= bytes_to_skip;
+ }
+
+ if !runs.is_empty() {
+ runs[0].len += prefix_len;
+ } else {
+ runs.push(TextRun {
+ len: result.len(),
+ ..Default::default()
+ });
+ }
+
+ let total_run_len: usize = runs.iter().map(|r| r.len).sum();
+ if total_run_len != result.len() && !runs.is_empty() {
+ let diff = result.len() as isize - total_run_len as isize;
+ if let Some(last) = runs.last_mut() {
+ last.len = (last.len as isize + diff) as usize;
+ }
+ }
+}
+
/// A fragment of a line that can be wrapped.
pub enum LineFragment<'a> {
/// A text fragment consisting of characters.
@@ -56,6 +56,12 @@ impl Label {
pub fn set_text(&mut self, text: impl Into<SharedString>) {
self.label = text.into();
}
+
+ /// Truncates the label from the start, keeping the end visible.
+ pub fn truncate_start(mut self) -> Self {
+ self.base = self.base.truncate_start();
+ self
+ }
}
// Style methods.
@@ -256,7 +262,8 @@ impl Component for Label {
"Special Cases",
vec![
single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
- single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
+ single_example("Regular Truncation", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
+ single_example("Start Truncation", div().max_w_24().child(Label::new("zed/crates/ui/src/components/label/truncate/label/label.rs").truncate_start()).into_any_element()),
],
),
])
@@ -88,6 +88,7 @@ pub struct LabelLike {
underline: bool,
single_line: bool,
truncate: bool,
+ truncate_start: bool,
}
impl Default for LabelLike {
@@ -113,6 +114,7 @@ impl LabelLike {
underline: false,
single_line: false,
truncate: false,
+ truncate_start: false,
}
}
}
@@ -126,6 +128,12 @@ impl LabelLike {
gpui::margin_style_methods!({
visibility: pub
});
+
+ /// Truncates overflowing text with an ellipsis (`β¦`) at the start if needed.
+ pub fn truncate_start(mut self) -> Self {
+ self.truncate_start = true;
+ self
+ }
}
impl LabelCommon for LabelLike {
@@ -169,7 +177,7 @@ impl LabelCommon for LabelLike {
self
}
- /// Truncates overflowing text with an ellipsis (`β¦`) if needed.
+ /// Truncates overflowing text with an ellipsis (`β¦`) at the end if needed.
fn truncate(mut self) -> Self {
self.truncate = true;
self
@@ -235,6 +243,9 @@ impl RenderOnce for LabelLike {
.when(self.truncate, |this| {
this.overflow_x_hidden().text_ellipsis()
})
+ .when(self.truncate_start, |this| {
+ this.overflow_x_hidden().text_ellipsis_start()
+ })
.text_color(color)
.font_weight(
self.weight