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:
1 parent
9d801eec7a
commit
8e29ede557
2 files changed
+98
-15
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user