terminal: Reduce flicker on resize (#47195)
Skip PTY resizes for pixel-only changes and coalesce pending resize events. Snap standalone terminal layout to whole device pixels to avoid subpixel jitter. before: https://github.com/user-attachments/assets/0ad0db83-0099-44c8-b8d1-3dc8146b25ef after: https://github.com/user-attachments/assets/86278014-1c87-4263-a9e5-b58bcc1fa2ea Release Notes: - Fixed: Reduce terminal flicker on resize --------- Signed-off-by: pigletfly <wangbing.adam@gmail.com> Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
1 parent
53e1473bea
commit
930c6f08b6
2 files changed
+118
-13
No files matched your search
@@ -1440,8 +1440,27 @@ impl Terminal {
|
||||
|
||||
///Resize the terminal and the PTY.
|
||||
pub fn set_size(&mut self, new_bounds: TerminalBounds) {
|
||||
if self.last_content.terminal_bounds != new_bounds {
|
||||
self.events.push_back(InternalEvent::Resize(new_bounds))
|
||||
let mut new_bounds = new_bounds;
|
||||
new_bounds.bounds.size.height = cmp::max(new_bounds.line_height, new_bounds.height());
|
||||
new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width());
|
||||
|
||||
let old_bounds = self.last_content.terminal_bounds;
|
||||
self.last_content.terminal_bounds = new_bounds;
|
||||
|
||||
// Avoid spamming PTY resizes on pixel-level size changes (e.g. while dragging edges),
|
||||
// since those can generate excessive SIGWINCH/reflows and cause visible flicker.
|
||||
let requires_resize = old_bounds.num_lines() != new_bounds.num_lines()
|
||||
|| old_bounds.num_columns() != new_bounds.num_columns()
|
||||
|| old_bounds.cell_width != new_bounds.cell_width
|
||||
|| old_bounds.line_height != new_bounds.line_height;
|
||||
|
||||
if !requires_resize {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.events.back_mut() {
|
||||
Some(InternalEvent::Resize(pending_bounds)) => *pending_bounds = new_bounds,
|
||||
_ => self.events.push_back(InternalEvent::Resize(new_bounds)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3036,6 +3055,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_set_size_coalesces_pixel_only_changes(cx: &mut TestAppContext) {
|
||||
let builder = cx.update(|cx| {
|
||||
TerminalBuilder::new_display_only(
|
||||
CursorShape::Block,
|
||||
AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
cx.background_executor(),
|
||||
PathStyle::local(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
let mut terminal = builder.terminal;
|
||||
|
||||
let base_bounds = TerminalBounds {
|
||||
cell_width: Pixels::from(10.),
|
||||
line_height: Pixels::from(10.),
|
||||
bounds: bounds(
|
||||
Point::default(),
|
||||
size(Pixels::from(100.), Pixels::from(100.)),
|
||||
),
|
||||
};
|
||||
|
||||
terminal.set_size(base_bounds);
|
||||
terminal.events.clear();
|
||||
assert_eq!(terminal.last_content.terminal_bounds, base_bounds);
|
||||
|
||||
// Pixel-only change: height grows by 1px but still the same number of rows/cols.
|
||||
let mut pixel_changed = base_bounds;
|
||||
pixel_changed.bounds.size.height = Pixels::from(101.);
|
||||
terminal.set_size(pixel_changed);
|
||||
assert!(terminal.events.is_empty());
|
||||
assert_eq!(terminal.last_content.terminal_bounds, pixel_changed);
|
||||
|
||||
// Grid change: height increases enough to add a row.
|
||||
let mut grid_changed = base_bounds;
|
||||
grid_changed.bounds.size.height = Pixels::from(110.);
|
||||
terminal.set_size(grid_changed);
|
||||
assert!(matches!(
|
||||
terminal.events.back(),
|
||||
Some(InternalEvent::Resize(_))
|
||||
));
|
||||
}
|
||||
|
||||
fn get_cells(size: TerminalBounds, rng: &mut StdRng) -> Vec<Vec<char>> {
|
||||
let mut cells = Vec::new();
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ pub struct LayoutState {
|
||||
mode: TermMode,
|
||||
display_offset: usize,
|
||||
hyperlink_tooltip: Option<AnyElement>,
|
||||
gutter: Pixels,
|
||||
block_below_cursor_element: Option<AnyElement>,
|
||||
base_text_style: TextStyle,
|
||||
content_mode: ContentMode,
|
||||
@@ -970,6 +969,7 @@ impl Element for TerminalElement {
|
||||
|
||||
let mut size = bounds.size;
|
||||
size.width -= gutter;
|
||||
let available_height = size.height;
|
||||
|
||||
// https://github.com/zed-industries/zed/issues/2750
|
||||
// if the terminal is one column wide, rendering 🦀
|
||||
@@ -981,6 +981,43 @@ impl Element for TerminalElement {
|
||||
let mut origin = bounds.origin;
|
||||
origin.x += gutter;
|
||||
|
||||
if matches!(self.terminal_view.read(cx).mode, TerminalMode::Standalone) {
|
||||
let scale_factor = window.scale_factor();
|
||||
let line_height_pixels = px(line_height);
|
||||
let line_height_device_px = (f32::from(line_height_pixels) * scale_factor)
|
||||
.round()
|
||||
.max(1.0) as i32;
|
||||
let available_height_device_px =
|
||||
(f32::from(available_height) * scale_factor)
|
||||
.floor()
|
||||
.max(0.0) as i32;
|
||||
|
||||
let rows =
|
||||
((available_height_device_px / line_height_device_px) as usize).max(1);
|
||||
let snapped_height_device_px = (rows as i32) * line_height_device_px;
|
||||
let padding_device_px =
|
||||
(available_height_device_px - snapped_height_device_px).max(0);
|
||||
|
||||
let snapped_height =
|
||||
px(snapped_height_device_px as f32 / scale_factor.max(1.0));
|
||||
let padding = px(padding_device_px as f32 / scale_factor.max(1.0));
|
||||
|
||||
size.height = snapped_height;
|
||||
if self.terminal.read(cx).scrolled_to_bottom() {
|
||||
origin.y += padding;
|
||||
}
|
||||
}
|
||||
|
||||
// Snap to device pixels to avoid subpixel jitter while resizing.
|
||||
// Terminal rendering is grid-based; allowing fractional origins can cause the
|
||||
// glyph rasterization to shift between frames, which looks like flicker.
|
||||
let scale_factor = window.scale_factor();
|
||||
let snap_px = |value: Pixels| {
|
||||
Pixels::from((f32::from(value) * scale_factor).floor() / scale_factor)
|
||||
};
|
||||
origin.x = snap_px(origin.x);
|
||||
origin.y = snap_px(origin.y);
|
||||
|
||||
(
|
||||
TerminalBounds::new(px(line_height), cell_width, Bounds { origin, size }),
|
||||
line_height,
|
||||
@@ -1018,7 +1055,7 @@ impl Element for TerminalElement {
|
||||
|
||||
let scroll_top = self.terminal_view.read(cx).scroll_top;
|
||||
let hyperlink_tooltip = hover_tooltip.map(|hover_tooltip| {
|
||||
let offset = bounds.origin + point(gutter, px(0.)) - point(px(0.), scroll_top);
|
||||
let offset = dimensions.bounds.origin - point(px(0.), scroll_top);
|
||||
let mut element = div()
|
||||
.size_full()
|
||||
.id("terminal-element")
|
||||
@@ -1062,8 +1099,9 @@ impl Element for TerminalElement {
|
||||
//
|
||||
// This optimization is analogous to the editor optimization in PR #45077
|
||||
// which fixed performance issues with large AutoHeight editors inside Lists.
|
||||
let content_bounds = dimensions.bounds;
|
||||
let visible_bounds = window.content_mask().bounds;
|
||||
let intersection = visible_bounds.intersect(&bounds);
|
||||
let intersection = visible_bounds.intersect(&content_bounds);
|
||||
|
||||
// If the terminal is entirely outside the viewport, skip all cell processing.
|
||||
// This handles the case where the terminal has been scrolled past (above or
|
||||
@@ -1073,7 +1111,7 @@ impl Element for TerminalElement {
|
||||
|| intersection.size.width <= px(0.)
|
||||
{
|
||||
(Vec::new(), Vec::new())
|
||||
} else if intersection == bounds {
|
||||
} else if intersection == content_bounds {
|
||||
// Fast path: terminal fully visible, no clipping needed.
|
||||
// Avoid grouping/allocation overhead by streaming cells directly.
|
||||
TerminalElement::layout_grid(
|
||||
@@ -1092,9 +1130,9 @@ impl Element for TerminalElement {
|
||||
// by screen position (enumerated line group index), not by the cell's
|
||||
// internal line number (which can be negative in Scrollable mode for
|
||||
// scrollback history).
|
||||
let rows_above_viewport =
|
||||
f32::from((intersection.top() - bounds.top()).max(px(0.)) / line_height_px)
|
||||
as usize;
|
||||
let rows_above_viewport = f32::from(
|
||||
(intersection.top() - content_bounds.top()).max(px(0.)) / line_height_px,
|
||||
) as usize;
|
||||
let visible_row_count =
|
||||
f32::from((intersection.size.height / line_height_px).ceil()) as usize + 1;
|
||||
|
||||
@@ -1199,7 +1237,7 @@ impl Element for TerminalElement {
|
||||
block.height as f32 * dimensions.line_height(),
|
||||
),
|
||||
);
|
||||
let origin = bounds.origin
|
||||
let origin = Point::new(bounds.origin.x, dimensions.bounds.origin.y)
|
||||
+ point(px(0.), target_line as f32 * dimensions.line_height())
|
||||
- point(px(0.), scroll_top);
|
||||
window.with_rem_size(rem_size, |window| {
|
||||
@@ -1225,7 +1263,6 @@ impl Element for TerminalElement {
|
||||
mode,
|
||||
display_offset,
|
||||
hyperlink_tooltip,
|
||||
gutter,
|
||||
block_below_cursor_element,
|
||||
base_text_style: text_style,
|
||||
content_mode,
|
||||
@@ -1249,8 +1286,12 @@ impl Element for TerminalElement {
|
||||
let scroll_top = self.terminal_view.read(cx).scroll_top;
|
||||
|
||||
window.paint_quad(fill(bounds, layout.background_color));
|
||||
let origin =
|
||||
bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top);
|
||||
let origin = layout.dimensions.bounds.origin - Point::new(px(0.), scroll_top);
|
||||
let scale_factor = window.scale_factor();
|
||||
let snap_px = |value: Pixels| {
|
||||
Pixels::from((f32::from(value) * scale_factor).floor() / scale_factor)
|
||||
};
|
||||
let origin = point(snap_px(origin.x), snap_px(origin.y));
|
||||
|
||||
let marked_text_cloned: Option<String> = {
|
||||
let ime_state = &self.terminal_view.read(cx).ime_state;
|
||||
|
||||
Reference in new issue
Block a user