From ac2065e7e92104e85524bc8a9d63b408bb39ac09 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 15 Feb 2022 17:09:25 +0100 Subject: [PATCH 1/3] Scroll so that text cursor remains visible Closes https://github.com/emilk/egui/issues/165 --- CHANGELOG.md | 17 ++++++++------ egui/src/containers/scroll_area.rs | 19 ++++++++------- egui/src/response.rs | 7 +++--- egui/src/ui.rs | 34 +++++++++++++++++++++++---- egui/src/widgets/text_edit/builder.rs | 8 +++++-- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f577bbdbc81..aa86e9d9313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,27 +8,29 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ## Unreleased ### Added ⭐ -* `Ui::input_mut` to modify how subsequent widgets see the `InputState` and a convenience method `InputState::consume_key` for shortcuts or hotkeys ([#1212](https://github.com/emilk/egui/pull/1212)). * Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)): * You can now select any font size and family using `RichText::size` amd `RichText::family` and the new `FontId`. * Easily change text styles with `Style::text_styles`. * Added `Ui::text_style_height`. * Added `TextStyle::resolve`. -* Made v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)). + * Made v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)). +* Plot: + * Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)). + * Added `Plot::allow_boxed_zoom()`, `Plot::boxed_zoom_pointer()` for boxed zooming on plots ([#1188](https://github.com/emilk/egui/pull/1188)). + * Added plot pointer coordinates with `Plot::coordinates_formatter`. ([#1235](https://github.com/emilk/egui/pull/1235)). + * Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)). * `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)). +* `Ui::input_mut` to modify how subsequent widgets see the `InputState` and a convenience method `InputState::consume_key` for shortcuts or hotkeys ([#1212](https://github.com/emilk/egui/pull/1212)). * Added `Ui::add_visible` and `Ui::add_visible_ui`. * Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)). * Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)). -* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)). * Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)). -* Added `Plot::allow_boxed_zoom()`, `Plot::boxed_zoom_pointer()` for boxed zooming on plots ([#1188](https://github.com/emilk/egui/pull/1188)). -* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)). * Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)). * Added `ui.weak(text)`. * Added `Context::move_to_top` and `Context::top_most_layer` for managing the layer on the top ([#1242](https://github.com/emilk/egui/pull/1242)). -* Added plot pointer coordinates with `Plot::coordinates_formatter`. ([#1235](https://github.com/emilk/egui/pull/1235)). -* Added `Slider::step_by` ([1255](https://github.com/emilk/egui/pull/1225)). +* Added `Slider::step_by` ([1225](https://github.com/emilk/egui/pull/1225)). * Added ability to scroll an UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247)). +* Added `Ui::scroll_to_rect` ([1252](https://github.com/emilk/egui/pull/1252)). ### Changed 🔧 * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! @@ -60,6 +62,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Fixed `enable_drag` for Windows ([#1108](https://github.com/emilk/egui/pull/1108)). * Calling `Context::set_pixels_per_point` before the first frame will now work. * Tooltips that don't fit the window don't flicker anymore ([#1240](https://github.com/emilk/egui/pull/1240)). +* Scroll areas now follow text cursor ([1252](https://github.com/emilk/egui/pull/1252)). ### Contributors 🙏 * [AlexxxRu](https://github.com/alexxxru): [#1108](https://github.com/emilk/egui/pull/1108). diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index daa6b83edfe..41681d958bf 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -498,7 +498,7 @@ impl Prepared { let clip_end = clip_rect.max[d]; let mut spacing = ui.spacing().item_spacing[d]; - if let Some(align) = align { + let delta = if let Some(align) = align { let center_factor = align.to_factor(); let offset = @@ -507,19 +507,20 @@ impl Prepared { // Depending on the alignment we need to add or subtract the spacing spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); - state.offset[d] = offset + spacing; + offset + spacing } else if start < clip_start && end < clip_end { - let min_adjust = - (clip_start - start + spacing).min(clip_end - end - spacing); - state.offset[d] -= min_adjust; + -(clip_start - start + spacing).min(clip_end - end - spacing) } else if end > clip_end && start > clip_start { - let min_adjust = - (end - clip_end + spacing).min(start - clip_start - spacing); - state.offset[d] += min_adjust; + (end - clip_end + spacing).min(start - clip_start - spacing) } else { // Ui is already in view, no need to adjust scroll. - continue; + 0.0 }; + + if delta != 0.0 { + state.offset[d] += delta; + ui.ctx().request_repaint(); + } } } } diff --git a/egui/src/response.rs b/egui/src/response.rs index e85626f690b..5478fe10353 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -443,10 +443,11 @@ impl Response { ) } - /// Adjust the scroll position until this UI becomes visible. If `align` is not provided, it'll scroll enough to - /// bring the UI into view. + /// Adjust the scroll position until this UI becomes visible. /// - /// See also [`Ui::scroll_to_cursor`] + /// If `align` is `None`, it'll scroll enough to bring the UI into view. + /// + /// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to`]. /// /// ``` /// # egui::__run_test_ui(|ui| { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 2c14a3dac50..34055542080 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -904,10 +904,36 @@ impl Ui { (response, painter) } - /// Adjust the scroll position until the cursor becomes visible. If `align` is not provided, it'll scroll enough to - /// bring the cursor into view. + /// Adjust the scroll position of any parent [`ScrollArea`] so that the given `Rect` becomes visible. /// - /// See also [`Response::scroll_to_me`] + /// If `align` is `None`, it'll scroll enough to bring the cursor into view. + /// + /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to`]. + /// + /// ``` + /// # use egui::Align; + /// # egui::__run_test_ui(|ui| { + /// egui::ScrollArea::vertical().show(ui, |ui| { + /// // … + /// let response = ui.button("Center on me.").clicked(); + /// if response.clicked() { + /// ui.scroll_to_rect(response.rect, Some(Align::CENTER)); + /// } + /// }); + /// # }); + /// ``` + pub fn scroll_to_rect(&self, rect: Rect, align: Option) { + for d in 0..2 { + let range = rect.min[d]..=rect.max[d]; + self.ctx().frame_state().scroll_target[d] = Some((range, align)); + } + } + + /// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible. + /// + /// If `align` is not provided, it'll scroll enough to bring the cursor into view. + /// + /// See also: [`Response::scroll_to_me`], [`Ui::scroll_to`]. /// /// ``` /// # use egui::Align; @@ -924,7 +950,7 @@ impl Ui { /// }); /// # }); /// ``` - pub fn scroll_to_cursor(&mut self, align: Option) { + pub fn scroll_to_cursor(&self, align: Option) { let target = self.next_widget_position(); for d in 0..2 { let target = target[d]; diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index 1c95663380e..da0d577b673 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -561,7 +561,7 @@ impl<'t> TextEdit<'t> { // We paint the cursor on top of the text, in case // the text galley has backgrounds (as e.g. `code` snippets in markup do). paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursor_range); - paint_cursor_end( + let cursor_pos = paint_cursor_end( ui, row_height, &painter, @@ -570,6 +570,8 @@ impl<'t> TextEdit<'t> { &cursor_range.primary, ); + ui.scroll_to_rect(cursor_pos, None); // keep cursor in view + if interactive && text.is_mutable() { // egui_web uses `text_cursor_pos` when showing IME, // so only set it when text is editable and visible! @@ -887,7 +889,7 @@ fn paint_cursor_end( pos: Pos2, galley: &Galley, cursor: &Cursor, -) { +) -> Rect { let stroke = ui.visuals().selection.stroke; let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2()); @@ -915,6 +917,8 @@ fn paint_cursor_end( (width, stroke.color), ); } + + cursor_pos } // ---------------------------------------------------------------------------- From 54f12c90d9c96fcf9af726ca871626c8d31bc0d3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 15 Feb 2022 22:40:51 +0100 Subject: [PATCH 2/3] fix doctest --- egui/src/ui.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 34055542080..ae0d26cb5be 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -915,9 +915,9 @@ impl Ui { /// # egui::__run_test_ui(|ui| { /// egui::ScrollArea::vertical().show(ui, |ui| { /// // … - /// let response = ui.button("Center on me.").clicked(); + /// let response = ui.button("Center on me."); /// if response.clicked() { - /// ui.scroll_to_rect(response.rect, Some(Align::CENTER)); + /// ui.scroll_to_rect(response.rect, Some(Align::Center)); /// } /// }); /// # }); From acedd8bc58ca491d189ac2284852c4d662f5f776 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 16 Feb 2022 10:28:56 +0100 Subject: [PATCH 3/3] Only follow cursor if text or cursor changes --- egui/src/widgets/text_edit/builder.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index da0d577b673..6dd52896ffe 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -543,6 +543,14 @@ impl<'t> TextEdit<'t> { text_draw_pos -= vec2(offset_x, 0.0); } + let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) = + (cursor_range, prev_cursor_range) + { + prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range() + } else { + false + }; + if ui.is_rect_visible(rect) { painter.galley(text_draw_pos, galley.clone()); @@ -570,17 +578,14 @@ impl<'t> TextEdit<'t> { &cursor_range.primary, ); - ui.scroll_to_rect(cursor_pos, None); // keep cursor in view + if response.changed || selection_changed { + ui.scroll_to_rect(cursor_pos, None); // keep cursor in view + } if interactive && text.is_mutable() { // egui_web uses `text_cursor_pos` when showing IME, // so only set it when text is editable and visible! - ui.ctx().output().text_cursor_pos = Some( - galley - .pos_from_cursor(&cursor_range.primary) - .translate(response.rect.min.to_vec2()) - .left_top(), - ); + ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top()); } } } @@ -588,14 +593,6 @@ impl<'t> TextEdit<'t> { state.clone().store(ui.ctx(), id); - let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) = - (cursor_range, prev_cursor_range) - { - prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range() - } else { - false - }; - if response.changed { response.widget_info(|| { WidgetInfo::text_edit(