Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow scroll into view without specifying an alignment #1247

Merged
merged 9 commits into from Feb 15, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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!
Expand Down
35 changes: 27 additions & 8 deletions egui/src/containers/scroll_area.rs
Expand Up @@ -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;
};
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions 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.
Expand Down Expand Up @@ -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<f32>, Option<Align>)>; 2],
}

impl Default for FrameState {
Expand All @@ -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],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to change this because RangeInclusive is not Copy.

}
}
}
Expand All @@ -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.
Expand Down
18 changes: 9 additions & 9 deletions 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,
};
Expand Down Expand Up @@ -443,26 +443,26 @@ 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| {
/// egui::ScrollArea::vertical().show(ui, |ui| {
/// 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<Align>) {
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.
Expand Down
12 changes: 8 additions & 4 deletions egui/src/ui.rs
Expand Up @@ -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;
Expand All @@ -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<Align>) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs documentation for what align=None means

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added docs!

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));
}
}
}
Expand Down
17 changes: 10 additions & 7 deletions egui_demo_lib/src/apps/demo/scrolling.rs
Expand Up @@ -147,15 +147,15 @@ fn huge_content_painter(ui: &mut egui::Ui) {
#[derive(PartialEq)]
struct ScrollTo {
track_item: usize,
tack_item_align: Align,
tack_item_align: Option<Align>,
offset: f32,
}

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,
}
}
Expand All @@ -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();
});

Expand All @@ -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 {
Expand All @@ -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;
Expand Down