From cd0f5859b26b783e554be2af0d34cb8ea8e539fb Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Wed, 4 Dec 2024 08:18:49 -0500 Subject: [PATCH] Make text cursor always appear on click (#5420) * [x] I have followed the instructions in the PR template ### Problem When clicking on a TextEdit sometimes the cursor doesn't appear immediately which makes it feel like the click was not registered for a second. This is because the start time for the blinking animation is only reset on keyboard input, but not on mouse interaction. It's hard to tell on the video but the cursor doesn't show immediately after clicking if the blink timer happens to be off. https://github.com/user-attachments/assets/9f049bd0-0375-4291-b2ef-697777fb854d ### Solution Reset the click timer every time a `TextEdit` is clicked. Additionally, the cursor is now correctly painted on the pixel boundary. IMO we should default to 1px cursor (instead of 2px) but that's not included in this PR. Happy to make that change too. https://github.com/user-attachments/assets/6c489414-f2c4-4dc6-85dd-f8bc457edad0 --- crates/egui/src/text_selection/visuals.rs | 19 +++++++++++++++---- crates/egui/src/widgets/text_edit/builder.rs | 6 ++++-- crates/egui/src/widgets/text_edit/state.rs | 4 ++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index d86f9dc5..2a31d1e1 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -96,8 +96,19 @@ pub fn paint_text_selection( pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { let stroke = visuals.text_cursor.stroke; - let top = cursor_rect.center_top(); - let bottom = cursor_rect.center_bottom(); + // Ensure the cursor is aligned to the pixel grid for whole number widths. + // See https://github.com/emilk/egui/issues/5164 + let (top, bottom) = if (stroke.width as usize) % 2 == 0 { + ( + painter.round_pos_to_pixels(cursor_rect.center_top()), + painter.round_pos_to_pixels(cursor_rect.center_bottom()), + ) + } else { + ( + painter.round_pos_to_pixel_center(cursor_rect.center_top()), + painter.round_pos_to_pixel_center(cursor_rect.center_bottom()), + ) + }; painter.line_segment([top, bottom], (stroke.width, stroke.color)); @@ -121,14 +132,14 @@ pub fn paint_text_cursor( ui: &Ui, painter: &Painter, primary_cursor_rect: Rect, - time_since_last_edit: f64, + time_since_last_interaction: f64, ) { if ui.visuals().text_cursor.blink { let on_duration = ui.visuals().text_cursor.on_duration; let off_duration = ui.visuals().text_cursor.off_duration; let total_duration = on_duration + off_duration; - let time_in_cycle = (time_since_last_edit % (total_duration as f64)) as f32; + let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32; let wake_in = if time_in_cycle < on_duration { // Cursor is visible diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 7c37c6a6..012d8c8f 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -603,6 +603,8 @@ impl<'t> TextEdit<'t> { if did_interact || response.clicked() { ui.memory_mut(|mem| mem.request_focus(response.id)); + + state.last_interaction_time = ui.ctx().input(|i| i.time); } } } @@ -746,7 +748,7 @@ impl<'t> TextEdit<'t> { if text.is_mutable() && interactive { let now = ui.ctx().input(|i| i.time); if response.changed || selection_changed { - state.last_edit_time = now; + state.last_interaction_time = now; } // Only show (and blink) cursor if the egui viewport has focus. @@ -759,7 +761,7 @@ impl<'t> TextEdit<'t> { ui, &painter, primary_cursor_rect, - now - state.last_edit_time, + now - state.last_interaction_time, ); } diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index e73664a1..cbbb6071 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -53,10 +53,10 @@ pub struct TextEditState { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) singleline_offset: f32, - /// When did the user last press a key? + /// When did the user last press a key or click on the `TextEdit`. /// Used to pause the cursor animation when typing. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) last_edit_time: f64, + pub(crate) last_interaction_time: f64, } impl TextEditState {