From 4f7f23ef5eddd839654799d45ba377664131a7d1 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Mon, 2 Dec 2024 03:29:06 -0500 Subject: [PATCH] Fix cursor clipping in `TextEdit` inside a `ScrollArea` (#3660) * Closes #1531 ### Before Notice how the cursor hides after third enter and when the line is long. https://github.com/user-attachments/assets/8e45736e-d6c7-4dc6-94d0-213188c199ff ### After Cursor is always visible https://github.com/user-attachments/assets/43200683-3524-471b-990a-eb7b49385fa9 - `ScrollArea` now checks if there's a `scroll_target` in `begin`, if there is, it saves it because it's not from its children, then restore it in `end`. - `TextEdit` now allocates additional space if its galley grows during the frame. This is needed so that any surrounding `ScrollArea` can bring the cursor to view, otherwise the cursor lays outside the the `ScrollArea`'s `content_ui`. --- crates/egui/src/containers/scroll_area.rs | 22 +++++++++++++++++++- crates/egui/src/widgets/text_edit/builder.rs | 16 +++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 8b8bd8ad..6065092c 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -499,6 +499,11 @@ struct Prepared { scrolling_enabled: bool, stick_to_end: Vec2b, + + /// If there was a scroll target before the ScrollArea was added this frame, it's + /// not for us to handle so we save it and restore it after this ScrollArea is done. + saved_scroll_target: [Option; 2], + animated: bool, } @@ -693,6 +698,10 @@ impl ScrollArea { } } + let saved_scroll_target = content_ui + .ctx() + .pass_state_mut(|state| std::mem::take(&mut state.scroll_target)); + Prepared { id, state, @@ -707,6 +716,7 @@ impl ScrollArea { viewport, scrolling_enabled, stick_to_end, + saved_scroll_target, animated, } } @@ -820,6 +830,7 @@ impl Prepared { viewport: _, scrolling_enabled, stick_to_end, + saved_scroll_target, animated, } = self; @@ -853,7 +864,7 @@ impl Prepared { let (start, end) = (range.min, range.max); let clip_start = clip_rect.min[d]; let clip_end = clip_rect.max[d]; - let mut spacing = ui.spacing().item_spacing[d]; + let mut spacing = content_ui.spacing().item_spacing[d]; let delta_update = if let Some(align) = align { let center_factor = align.to_factor(); @@ -902,6 +913,15 @@ impl Prepared { } } + // Restore scroll target meant for ScrollAreas up the stack (if any) + ui.ctx().pass_state_mut(|state| { + for d in 0..2 { + if saved_scroll_target[d].is_some() { + state.scroll_target[d] = saved_scroll_target[d].clone(); + }; + } + }); + let inner_rect = { // At this point this is the available size for the inner rect. let mut inner_size = inner_rect.size(); diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 5ce5749b..7c37c6a6 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use emath::Rect; use epaint::text::{cursor::CCursor, Galley, LayoutJob}; use crate::{ @@ -720,6 +721,16 @@ impl<'t> TextEdit<'t> { } } + // Allocate additional space if edits were made this frame that changed the size. This is important so that, + // if there's a ScrollArea, it can properly scroll to the cursor. + let extra_size = galley.size() - rect.size(); + if extra_size.x > 0.0 || extra_size.y > 0.0 { + ui.allocate_rect( + Rect::from_min_size(outer_rect.max, extra_size), + Sense::hover(), + ); + } + painter.galley(galley_pos, galley.clone(), text_color); if has_focus { @@ -727,10 +738,9 @@ impl<'t> TextEdit<'t> { let primary_cursor_rect = cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); - let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531 - if (response.changed || selection_changed) && !is_fully_visible { + if response.changed || selection_changed { // Scroll to keep primary cursor in view: - ui.scroll_to_rect(primary_cursor_rect, None); + ui.scroll_to_rect(primary_cursor_rect + margin, None); } if text.is_mutable() && interactive {