diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc4bfe9e0c..d9871686a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * 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 `Slider::step_by` ([1255](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)) ### Changed 🔧 * ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index d5c9e712c84..daa6b83edfe 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -489,18 +489,37 @@ 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 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]; - // Depending on the alignment we need to add or subtract the spacing - spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); + if let Some(align) = align { + let center_factor = align.to_factor(); + + let offset = + lerp(scroll, center_factor) - lerp(visible_range, center_factor); - state.offset[d] = offset + spacing; + // 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; + }; } } } 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..e85626f690b 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, }; @@ -443,7 +443,10 @@ 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. + /// + /// See also [`Ui::scroll_to_cursor`] /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -451,18 +454,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..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; @@ -901,15 +904,16 @@ impl Ui { /// } /// /// if scroll_bottom { - /// ui.scroll_to_cursor(Align::BOTTOM); + /// ui.scroll_to_cursor(Some(Align::BOTTOM)); /// } /// }); /// # }); /// ``` - 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)); } } } 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;