From 32600f255aea17ffc4557b39f2fc8c275db3b59a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:14:31 -0300 Subject: [PATCH] gpui: Fix truncation flickering (#45373) It's been a little that we've noticed some flickering and other weird resizing behavior with text truncation in Zed: https://github.com/user-attachments/assets/4d5691a3-cd3d-45e0-8b96-74a4e0e273d2 https://github.com/user-attachments/assets/d1d0e587-7676-4da0-8818-f4e50f0e294e Initially, we suspected this could be due to how we calculate the length of a line to insert truncation, which is based first on the length of each individual character, and then second goes through a pass calculating the line length as a whole. This could cause mismatch and culminate in our bug. However, even though that felt like a reasonable suspicion, I realized something rather simple at some point: the `truncate` and `truncate_start` methods in the `Label` didn't use `whitespace_nowrap`. If you take Tailwind as an example, their `truncate` utility class takes `overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`. This pointed out to a potential bug with `whitespace_nowrap` where that was blocking truncation entirely, even though that's technically part of what's necessary to truncate as you don't want text that will be truncated to wrap. Ultimately, what was happening was that the text element was caching its layout based on its `wrap_width` but not considering its `truncate_width`. The truncate width is essentially the new definitive width of the text based on the available space, which was never being computed. So the fix here was to add `truncate_width.is_none()` to the cache validation check, so that it only uses the cached text element size _if the truncation width is untouched_. But if that changes, we need to account for the new width. Then, in the Label component, we added `min_w_0` to allow the label div to shrink below its original size, and finally, we added `whitespace_nowrap()` as the cache check fundamentally fixed that method's problem. In a future PR, we can basically remove the `single_line()` label method because: 1) whenever you want a single label, you most likely want it to truncate, and 2) most instances of `truncate` are already followed by `single_line` in Zed today, so we can cut that part. Result is no flickering with truncated labels! https://github.com/user-attachments/assets/ae17cbde-0de7-42ca-98a4-22fcb452016b Release Notes: - Fixed a bug in GPUI where truncated text would flicker as you resized the container in which the text was in. Co-authored-by: Lukas Wirth --- crates/gpui/src/elements/text.rs | 10 ++++++++-- crates/ui/src/components/label/label_like.rs | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 942a0a326526431dc65f389e9cff67bac252d571..770c1f871432afbecc9ffd4e903dfeddcfcba6ee 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -372,11 +372,17 @@ impl TextLayout { (None, "".into(), TruncateFrom::End) }; + // Only use cached layout if: + // 1. We have a cached size + // 2. wrap_width matches (or both are None) + // 3. truncate_width is None (if truncate_width is Some, we need to re-layout + // because the previous layout may have been computed without truncation) if let Some(text_layout) = element_state.0.borrow().as_ref() - && text_layout.size.is_some() + && let Some(size) = text_layout.size && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) + && truncate_width.is_none() { - return text_layout.size.unwrap(); + return size; } let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index f6e7a1b893d54fff425618d5c604f591144a7385..03fde4083d5e9a8e07f38c830edd5116f14e6d70 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -241,10 +241,16 @@ impl RenderOnce for LabelLike { .when(self.strikethrough, |this| this.line_through()) .when(self.single_line, |this| this.whitespace_nowrap()) .when(self.truncate, |this| { - this.overflow_x_hidden().text_ellipsis() + this.min_w_0() + .overflow_x_hidden() + .whitespace_nowrap() + .text_ellipsis() }) .when(self.truncate_start, |this| { - this.overflow_x_hidden().text_ellipsis_start() + this.min_w_0() + .overflow_x_hidden() + .whitespace_nowrap() + .text_ellipsis_start() }) .text_color(color) .font_weight(