Fix cursor clipping in `TextEdit` inside a `ScrollArea` (#3660)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to add commits to your PR.
* Remember to run `cargo fmt` and `cargo cranky`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* 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`.
This commit is contained in:
Juan Campa 2024-12-02 03:29:06 -05:00 committed by GitHub
parent 6833cf56e1
commit 4f7f23ef5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 4 deletions

View File

@ -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<pass_state::ScrollTarget>; 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();

View File

@ -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 {