Skip to content

Commit

Permalink
Fix cursor clipping in TextEdit inside a ScrollArea (#3660)
Browse files Browse the repository at this point in the history
<!--
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`.
  • Loading branch information
juancampa authored Dec 2, 2024
1 parent 6833cf5 commit 4f7f23e
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 4 deletions.
22 changes: 21 additions & 1 deletion crates/egui/src/containers/scroll_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
Expand All @@ -707,6 +716,7 @@ impl ScrollArea {
viewport,
scrolling_enabled,
stick_to_end,
saved_scroll_target,
animated,
}
}
Expand Down Expand Up @@ -820,6 +830,7 @@ impl Prepared {
viewport: _,
scrolling_enabled,
stick_to_end,
saved_scroll_target,
animated,
} = self;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
16 changes: 13 additions & 3 deletions crates/egui/src/widgets/text_edit/builder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::sync::Arc;

use emath::Rect;
use epaint::text::{cursor::CCursor, Galley, LayoutJob};

use crate::{
Expand Down Expand Up @@ -720,17 +721,26 @@ 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 {
if let Some(cursor_range) = state.cursor.range(&galley) {
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 {
Expand Down

0 comments on commit 4f7f23e

Please sign in to comment.