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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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