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 <me@lukaswirth.dev>
@@ -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);