From 19f25614ea6030468ccd894cfd7f04f0fa9ee165 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 09:34:34 -0500 Subject: [PATCH 1/8] Allow scroll into view without specifying an alignment --- egui/src/containers/scroll_area.rs | 21 +++++++++++++++++---- egui/src/frame_state.rs | 8 +++++--- egui/src/response.rs | 14 ++++++-------- egui/src/ui.rs | 5 +++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index d5c9e712c84..773e52d6afa 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -489,11 +489,24 @@ impl Prepared { // We take the scroll target so only this ScrollArea will use it: let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take(); if let Some((scroll, align)) = scroll_target { - let center_factor = align.to_factor(); - let min = content_ui.min_rect().min[d]; - let visible_range = min..=min + content_ui.clip_rect().size()[d]; - let offset = scroll - lerp(visible_range, center_factor); + let clip_rect = content_ui.clip_rect(); + let visible_range = min..=min + clip_rect.size()[d]; + + let center_factor = if let Some(align) = align { + align.to_factor() + } else { + if *scroll.start() < clip_rect.min[d] { + 0.0 + } else if *scroll.end() > clip_rect.max[d] { + 1.0 + } else { + // Ui os already in view, no need to adjust scroll offset. + continue; + } + }; + + let offset = lerp(scroll, center_factor) - lerp(visible_range, center_factor); let mut spacing = ui.spacing().item_spacing[d]; diff --git a/egui/src/frame_state.rs b/egui/src/frame_state.rs index 848dc41597b..e522eef65b2 100644 --- a/egui/src/frame_state.rs +++ b/egui/src/frame_state.rs @@ -1,3 +1,5 @@ +use std::ops::RangeInclusive; + use crate::*; /// State that is collected during a frame and then cleared. @@ -28,7 +30,7 @@ pub(crate) struct FrameState { /// Cleared by the first `ScrollArea` that makes use of it. pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ? /// horizontal, vertical - pub(crate) scroll_target: [Option<(f32, Align)>; 2], + pub(crate) scroll_target: [Option<(RangeInclusive, Option)>; 2], } impl Default for FrameState { @@ -40,7 +42,7 @@ impl Default for FrameState { used_by_panels: Rect::NAN, tooltip_rect: None, scroll_delta: Vec2::ZERO, - scroll_target: [None; 2], + scroll_target: [None, None], } } } @@ -63,7 +65,7 @@ impl FrameState { *used_by_panels = Rect::NOTHING; *tooltip_rect = None; *scroll_delta = input.scroll_delta; - *scroll_target = [None; 2]; + *scroll_target = [None, None]; } /// How much space is still available after panels has been added. diff --git a/egui/src/response.rs b/egui/src/response.rs index d685174f3eb..8c070e062fe 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -443,7 +443,8 @@ impl Response { ) } - /// Move the scroll to this UI with the specified alignment. + /// Adjust the scroll position until this UI becomes visible. If `align` is not provided, it'll scroll enough to + /// bring the UI into view. /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -451,18 +452,15 @@ impl Response { /// for i in 0..1000 { /// let response = ui.button("Scroll to me"); /// if response.clicked() { - /// response.scroll_to_me(egui::Align::Center); + /// response.scroll_to_me(Some(egui::Align::Center)); /// } /// } /// }); /// # }); /// ``` - pub fn scroll_to_me(&self, align: Align) { - let scroll_target = lerp(self.rect.x_range(), align.to_factor()); - self.ctx.frame_state().scroll_target[0] = Some((scroll_target, align)); - - let scroll_target = lerp(self.rect.y_range(), align.to_factor()); - self.ctx.frame_state().scroll_target[1] = Some((scroll_target, align)); + pub fn scroll_to_me(&self, align: Option) { + self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align)); + self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align)); } /// For accessibility. diff --git a/egui/src/ui.rs b/egui/src/ui.rs index b1790e8cb32..6cc5e6608e0 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -906,10 +906,11 @@ impl Ui { /// }); /// # }); /// ``` - pub fn scroll_to_cursor(&mut self, align: Align) { + pub fn scroll_to_cursor(&mut self, align: Option) { let target = self.next_widget_position(); for d in 0..2 { - self.ctx().frame_state().scroll_target[d] = Some((target[d], align)); + let target = target[d]; + self.ctx().frame_state().scroll_target[d] = Some((target..=target, align)); } } } From 2e0708deee295f1e5eec9c8be1fd1bf40e9c88eb Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 09:41:37 -0500 Subject: [PATCH 2/8] Adjust scrolling demo --- egui/src/response.rs | 2 +- egui_demo_lib/src/apps/demo/scrolling.rs | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/egui/src/response.rs b/egui/src/response.rs index 8c070e062fe..2038c10d775 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -1,5 +1,5 @@ use crate::{ - emath::{lerp, Align, Pos2, Rect, Vec2}, + emath::{Align, Pos2, Rect, Vec2}, menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText, NUM_POINTER_BUTTONS, }; diff --git a/egui_demo_lib/src/apps/demo/scrolling.rs b/egui_demo_lib/src/apps/demo/scrolling.rs index 0c01ef11699..d806d5160de 100644 --- a/egui_demo_lib/src/apps/demo/scrolling.rs +++ b/egui_demo_lib/src/apps/demo/scrolling.rs @@ -147,7 +147,7 @@ fn huge_content_painter(ui: &mut egui::Ui) { #[derive(PartialEq)] struct ScrollTo { track_item: usize, - tack_item_align: Align, + tack_item_align: Option, offset: f32, } @@ -155,7 +155,7 @@ impl Default for ScrollTo { fn default() -> Self { Self { track_item: 25, - tack_item_align: Align::Center, + tack_item_align: Some(Align::Center), offset: 0.0, } } @@ -180,13 +180,16 @@ impl super::View for ScrollTo { ui.horizontal(|ui| { ui.label("Item align:"); track_item |= ui - .radio_value(&mut self.tack_item_align, Align::Min, "Top") + .radio_value(&mut self.tack_item_align, Some(Align::Min), "Top") .clicked(); track_item |= ui - .radio_value(&mut self.tack_item_align, Align::Center, "Center") + .radio_value(&mut self.tack_item_align, Some(Align::Center), "Center") .clicked(); track_item |= ui - .radio_value(&mut self.tack_item_align, Align::Max, "Bottom") + .radio_value(&mut self.tack_item_align, Some(Align::Max), "Bottom") + .clicked(); + track_item |= ui + .radio_value(&mut self.tack_item_align, None, "None (Bring into view)") .clicked(); }); @@ -213,7 +216,7 @@ impl super::View for ScrollTo { let (current_scroll, max_scroll) = scroll_area .show(ui, |ui| { if scroll_top { - ui.scroll_to_cursor(Align::TOP); + ui.scroll_to_cursor(Some(Align::TOP)); } ui.vertical(|ui| { for item in 1..=50 { @@ -228,7 +231,7 @@ impl super::View for ScrollTo { }); if scroll_bottom { - ui.scroll_to_cursor(Align::BOTTOM); + ui.scroll_to_cursor(Some(Align::BOTTOM)); } let margin = ui.visuals().clip_rect_margin; From f62292f41ac74eadcdc97c9793ef65d50177476b Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 09:55:47 -0500 Subject: [PATCH 3/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb02be9a55..5f769a30956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * 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 ability to scroll an UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247)) ### Changed 🔧 * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! From b355a86fb967f101bbc733e000fbc1701128e4de Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 10:10:09 -0500 Subject: [PATCH 4/8] Fix test --- egui/src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 6cc5e6608e0..9cebfcf67a0 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -901,7 +901,7 @@ impl Ui { /// } /// /// if scroll_bottom { - /// ui.scroll_to_cursor(Align::BOTTOM); + /// ui.scroll_to_cursor(Some(Align::BOTTOM)); /// } /// }); /// # }); From 9e60f6d542df2bf86669cdcfd57877dcdadf8e1b Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 10:33:48 -0500 Subject: [PATCH 5/8] Clippy run --- egui/src/containers/scroll_area.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 773e52d6afa..1e669202371 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -495,15 +495,13 @@ impl Prepared { let center_factor = if let Some(align) = align { align.to_factor() + } else if *scroll.start() < clip_rect.min[d] { + 0.0 + } else if *scroll.end() > clip_rect.max[d] { + 1.0 } else { - if *scroll.start() < clip_rect.min[d] { - 0.0 - } else if *scroll.end() > clip_rect.max[d] { - 1.0 - } else { - // Ui os already in view, no need to adjust scroll offset. - continue; - } + // Ui os already in view, no need to adjust scroll offset. + continue; }; let offset = lerp(scroll, center_factor) - lerp(visible_range, center_factor); From 83c0d19d8a69895ec9f2a1fa2ffc41e8cdc7094b Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 15:32:14 -0500 Subject: [PATCH 6/8] Handle case of UI being too big to fit in the scroll view --- egui/src/containers/scroll_area.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 1e669202371..1de06c5d003 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -493,14 +493,17 @@ impl Prepared { let clip_rect = content_ui.clip_rect(); let visible_range = min..=min + clip_rect.size()[d]; + // if the ui is too big to completely fit in the scroll view, align it to the left/top + let too_big = (scroll.end() - scroll.start()) > clip_rect.size()[d]; + let center_factor = if let Some(align) = align { align.to_factor() - } else if *scroll.start() < clip_rect.min[d] { + } else if too_big || *scroll.start() < clip_rect.min[d] { 0.0 } else if *scroll.end() > clip_rect.max[d] { 1.0 } else { - // Ui os already in view, no need to adjust scroll offset. + // Ui is already in view, no need to adjust scroll. continue; }; From 9526a2c5b01e8e014ee08a1cd79b47e79c1e4468 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 15:37:36 -0500 Subject: [PATCH 7/8] Improve docs --- egui/src/response.rs | 2 ++ egui/src/ui.rs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/egui/src/response.rs b/egui/src/response.rs index 2038c10d775..e85626f690b 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -446,6 +446,8 @@ 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. /// + /// See also [`Ui::scroll_to_cursor`] + /// /// ``` /// # egui::__run_test_ui(|ui| { /// egui::ScrollArea::vertical().show(ui, |ui| { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 9cebfcf67a0..b1653a626c3 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -889,7 +889,10 @@ impl Ui { (response, painter) } - /// Move the scroll to this cursor position with the specified alignment. + /// Adjust the scroll position until the cursor becomes visible. If `align` is not provided, it'll scroll enough to + /// bring the cursor into view. + /// + /// See also [`Response::scroll_to_me`] /// /// ``` /// # use egui::Align; From e3d4222b1539bb0ddd6b7ec6ca3464d9810c9214 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 13 Feb 2022 17:01:18 -0500 Subject: [PATCH 8/8] Better handling for too big uis --- egui/src/containers/scroll_area.rs | 39 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index 1de06c5d003..daa6b83edfe 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -492,29 +492,34 @@ impl Prepared { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); let visible_range = min..=min + clip_rect.size()[d]; + let start = *scroll.start(); + let end = *scroll.end(); + let clip_start = clip_rect.min[d]; + let clip_end = clip_rect.max[d]; + let mut spacing = ui.spacing().item_spacing[d]; + + if let Some(align) = align { + let center_factor = align.to_factor(); - // if the ui is too big to completely fit in the scroll view, align it to the left/top - let too_big = (scroll.end() - scroll.start()) > clip_rect.size()[d]; + let offset = + lerp(scroll, center_factor) - lerp(visible_range, center_factor); - let center_factor = if let Some(align) = align { - align.to_factor() - } else if too_big || *scroll.start() < clip_rect.min[d] { - 0.0 - } else if *scroll.end() > clip_rect.max[d] { - 1.0 + // 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; + } 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; + } 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; } else { // Ui is already in view, no need to adjust scroll. continue; }; - - let offset = lerp(scroll, center_factor) - lerp(visible_range, center_factor); - - let mut spacing = ui.spacing().item_spacing[d]; - - // 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; } } }