From 8e29ede55760d25a8899590fa7a9dc1ad7381f87 Mon Sep 17 00:00:00 2001 From: linyisu Date: Tue, 22 Sep 2026 03:48:24 +0000 Subject: [PATCH] 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 --- crates/editor/src/cursor_animation.rs | 80 +++++++++++++++++++++++++++ crates/editor/src/element.rs | 33 ++++++----- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/cursor_animation.rs b/crates/editor/src/cursor_animation.rs index 0ea173c1e7..79a9faeae5 100644 --- a/crates/editor/src/cursor_animation.rs +++ b/crates/editor/src/cursor_animation.rs @@ -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, + corners: &[Point; 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(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f58057c803..0320af6ab8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, 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(