Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions crates/terminal/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,8 +1439,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)),
}
}

Expand Down Expand Up @@ -3034,6 +3053,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();

Expand Down
63 changes: 52 additions & 11 deletions crates/terminal_view/src/terminal_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -968,6 +967,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 🦀
Expand All @@ -979,6 +979,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,
Expand Down Expand Up @@ -1016,7 +1053,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")
Expand Down Expand Up @@ -1060,8 +1097,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
Expand All @@ -1071,7 +1109,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(
Expand All @@ -1090,9 +1128,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;

Expand Down Expand Up @@ -1195,7 +1233,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| {
Expand All @@ -1221,7 +1259,6 @@ impl Element for TerminalElement {
mode,
display_offset,
hyperlink_tooltip,
gutter,
block_below_cursor_element,
base_text_style: text_style,
content_mode,
Expand All @@ -1245,8 +1282,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;
Expand Down
Loading