editor: Paint block cursor text upon arrival during animation (#64521)

Follow-up to #63195.

# Objective
When `cursor_animation` is enabled, moving a block cursor (e.g. in Vim
mode) leaves the character underneath hidden until the spring animation
fully finishes, after which the letter pops in.
This happens because `CursorLayout::paint` returned early while
animating, skipping `block_text`.

## Solution
Paint `block_text` upon the animated cursor's arrival at the target
cell, rather than waiting for the entire spring settling phase to
complete:
- Check if any animated corner has reached the target bounds
(`bounds.dilate(...)`).
- If the cursor is still in mid-air, return early so we don't draw the
destination letter prematurely.
- Once it touches the target cell, fall through and paint `block_text`
over the cursor polygon.

## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase
Left: before, Right: after


https://github.com/user-attachments/assets/7057f40a-64a7-4035-b8e2-7c0b7dc40069

---

Release Notes:
- Fixed an issue where the block cursor hid the character underneath
until cursor animation completed.

---------

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
linyisuandChristopher Biscardi authored and GitHub committed 2026-09-22 03:48:24 +00:00
1 parent 9d801eec7a
commit 8e29ede557
2 files changed
+98 -15

No files matched your search

+80
View File
@@ -598,6 +598,35 @@ fn nearly_equal(left: f32, right: f32) -> bool {
(left - right).abs() <= GEOMETRY_EPSILON
}
pub(crate) fn animated_corners_overlap_target(
target_bounds: Bounds<Pixels>,
corners: &[Point<Pixels>; 4],
) -> bool {
let min_x = corners
.iter()
.map(|p| p.x)
.min()
.expect("the corners argument is a 4 item array, which should always have an x min");
let max_x = corners
.iter()
.map(|p| p.x)
.max()
.expect("the corners argument is a 4 item array, which should always have an x max");
let min_y = corners
.iter()
.map(|p| p.y)
.min()
.expect("the corners argument is a 4 item array, which should always have an y min");
let max_y = corners
.iter()
.map(|p| p.y)
.max()
.expect("the corners argument is a 4 item array, which should always have an x max");
let animated_bounds = Bounds::from_corners(point(min_x, min_y), point(max_x, max_y));
animated_bounds.intersects(&target_bounds)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -870,6 +899,57 @@ mod tests {
);
}
#[test]
fn one_cell_movement_overlaps_target_only_after_advancing() {
let now = Instant::now();
let mut state = CursorAnimationState::default();
let target = bounds_with_width(8.0, 0.0, 8.0);
state.update(
logical_position(0, 0),
bounds_with_width(0.0, 0.0, 8.0),
viewport(0.0),
now,
);
let corners = state
.update(logical_position(0, 1), target, viewport(0.0), now)
.unwrap();
assert!(!animated_corners_overlap_target(target, &corners));
let corners = state
.update(
logical_position(0, 1),
target,
viewport(0.0),
now + Duration::from_millis(16),
)
.unwrap();
assert!(animated_corners_overlap_target(target, &corners));
let mut vertical_state = CursorAnimationState::default();
let vertical_target = bounds_with_width(0.0, 20.0, 8.0);
vertical_state.update(
logical_position(0, 0),
bounds_with_width(0.0, 0.0, 8.0),
viewport(0.0),
now,
);
let corners = vertical_state
.update(logical_position(1, 0), vertical_target, viewport(0.0), now)
.unwrap();
assert!(!animated_corners_overlap_target(vertical_target, &corners));
let corners = vertical_state
.update(
logical_position(1, 0),
vertical_target,
viewport(0.0),
now + Duration::from_millis(16),
)
.unwrap();
assert!(animated_corners_overlap_target(vertical_target, &corners));
}
#[test]
fn geometry_change_without_logical_movement_snaps() {
let now = Instant::now();
+18 -15
View File
@@ -17,7 +17,7 @@ use crate::{
SelectionDragState, SizingBehavior, SoftWrap, ToPoint,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
column_pixels,
cursor_animation::{CursorViewport, LogicalCursorPosition},
cursor_animation::{CursorViewport, LogicalCursorPosition, animated_corners_overlap_target},
display_map::{
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
HighlightKey, HighlightedChunk, ToDisplayPoint,
@@ -11092,6 +11092,8 @@ impl CursorLayout {
}
pub fn paint(&mut self, origin: gpui::Point<Pixels>, window: &mut Window, cx: &mut App) {
let bounds = window.pixel_snap_bounds(self.bounds(origin));
if let Some(corners) = self.animated_corners {
let mut builder = gpui::PathBuilder::fill();
builder.add_polygon(&corners, true);
@@ -11100,25 +11102,26 @@ impl CursorLayout {
name.paint(window, cx);
}
window.paint_path(path, self.color);
return;
if !animated_corners_overlap_target(bounds, &corners) {
return;
}
}
}
let bounds = window.pixel_snap_bounds(self.bounds(origin));
//Draw background or border quad
let cursor = if matches!(self.shape, CursorShape::Hollow) {
outline(bounds, self.color, BorderStyle::Solid)
} else {
fill(bounds, self.color)
};
//Draw background or border quad
let cursor = if matches!(self.shape, CursorShape::Hollow) {
outline(bounds, self.color, BorderStyle::Solid)
} else {
fill(bounds, self.color)
};
if let Some(name) = &mut self.cursor_name {
name.paint(window, cx);
if let Some(name) = &mut self.cursor_name {
name.paint(window, cx);
}
window.paint_quad(cursor);
}
window.paint_quad(cursor);
if let Some(block_text) = &self.block_text {
block_text
.paint(