Allow scroll into view without specifying an alignment (#1247)
* Allow scroll into view without specifying an alignment * Handle case of UI being too big to fit in the scroll view
This commit is contained in:
parent
c1cd47e3a7
commit
635c65773d
6 changed files with 60 additions and 31 deletions
|
@ -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 `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 `ui.weak(text)`.
|
||||||
* Added `Slider::step_by` ([1255](https://github.com/emilk/egui/pull/1225)).
|
* 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 🔧
|
### Changed 🔧
|
||||||
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!
|
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!
|
||||||
|
|
|
@ -489,18 +489,37 @@ impl Prepared {
|
||||||
// We take the scroll target so only this ScrollArea will use it:
|
// We take the scroll target so only this ScrollArea will use it:
|
||||||
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
|
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
|
||||||
if let Some((scroll, align)) = scroll_target {
|
if let Some((scroll, align)) = scroll_target {
|
||||||
|
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();
|
let center_factor = align.to_factor();
|
||||||
|
|
||||||
let min = content_ui.min_rect().min[d];
|
let offset =
|
||||||
let visible_range = min..=min + content_ui.clip_rect().size()[d];
|
lerp(scroll, center_factor) - lerp(visible_range, center_factor);
|
||||||
let offset = scroll - 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
|
// Depending on the alignment we need to add or subtract the spacing
|
||||||
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
|
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
|
||||||
|
|
||||||
state.offset[d] = offset + spacing;
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// State that is collected during a frame and then cleared.
|
/// 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.
|
/// Cleared by the first `ScrollArea` that makes use of it.
|
||||||
pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ?
|
pub(crate) scroll_delta: Vec2, // TODO: move to a Mutex inside of `InputState` ?
|
||||||
/// horizontal, vertical
|
/// horizontal, vertical
|
||||||
pub(crate) scroll_target: [Option<(f32, Align)>; 2],
|
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FrameState {
|
impl Default for FrameState {
|
||||||
|
@ -40,7 +42,7 @@ impl Default for FrameState {
|
||||||
used_by_panels: Rect::NAN,
|
used_by_panels: Rect::NAN,
|
||||||
tooltip_rect: None,
|
tooltip_rect: None,
|
||||||
scroll_delta: Vec2::ZERO,
|
scroll_delta: Vec2::ZERO,
|
||||||
scroll_target: [None; 2],
|
scroll_target: [None, None],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,7 @@ impl FrameState {
|
||||||
*used_by_panels = Rect::NOTHING;
|
*used_by_panels = Rect::NOTHING;
|
||||||
*tooltip_rect = None;
|
*tooltip_rect = None;
|
||||||
*scroll_delta = input.scroll_delta;
|
*scroll_delta = input.scroll_delta;
|
||||||
*scroll_target = [None; 2];
|
*scroll_target = [None, None];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How much space is still available after panels has been added.
|
/// How much space is still available after panels has been added.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
emath::{lerp, Align, Pos2, Rect, Vec2},
|
emath::{Align, Pos2, Rect, Vec2},
|
||||||
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
|
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
|
||||||
NUM_POINTER_BUTTONS,
|
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| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
@ -451,18 +454,15 @@ impl Response {
|
||||||
/// for i in 0..1000 {
|
/// for i in 0..1000 {
|
||||||
/// let response = ui.button("Scroll to me");
|
/// let response = ui.button("Scroll to me");
|
||||||
/// if response.clicked() {
|
/// 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) {
|
pub fn scroll_to_me(&self, align: Option<Align>) {
|
||||||
let scroll_target = lerp(self.rect.x_range(), align.to_factor());
|
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
|
||||||
self.ctx.frame_state().scroll_target[0] = Some((scroll_target, align));
|
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
|
||||||
|
|
||||||
let scroll_target = lerp(self.rect.y_range(), align.to_factor());
|
|
||||||
self.ctx.frame_state().scroll_target[1] = Some((scroll_target, align));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For accessibility.
|
/// For accessibility.
|
||||||
|
|
|
@ -889,7 +889,10 @@ impl Ui {
|
||||||
(response, painter)
|
(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;
|
/// # use egui::Align;
|
||||||
|
@ -901,15 +904,16 @@ impl Ui {
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// if scroll_bottom {
|
/// 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>) {
|
||||||
let target = self.next_widget_position();
|
let target = self.next_widget_position();
|
||||||
for d in 0..2 {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ fn huge_content_painter(ui: &mut egui::Ui) {
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct ScrollTo {
|
struct ScrollTo {
|
||||||
track_item: usize,
|
track_item: usize,
|
||||||
tack_item_align: Align,
|
tack_item_align: Option<Align>,
|
||||||
offset: f32,
|
offset: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ impl Default for ScrollTo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
track_item: 25,
|
track_item: 25,
|
||||||
tack_item_align: Align::Center,
|
tack_item_align: Some(Align::Center),
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,13 +180,16 @@ impl super::View for ScrollTo {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Item align:");
|
ui.label("Item align:");
|
||||||
track_item |= ui
|
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();
|
.clicked();
|
||||||
track_item |= ui
|
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();
|
.clicked();
|
||||||
track_item |= ui
|
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();
|
.clicked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -213,7 +216,7 @@ impl super::View for ScrollTo {
|
||||||
let (current_scroll, max_scroll) = scroll_area
|
let (current_scroll, max_scroll) = scroll_area
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
if scroll_top {
|
if scroll_top {
|
||||||
ui.scroll_to_cursor(Align::TOP);
|
ui.scroll_to_cursor(Some(Align::TOP));
|
||||||
}
|
}
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
for item in 1..=50 {
|
for item in 1..=50 {
|
||||||
|
@ -228,7 +231,7 @@ impl super::View for ScrollTo {
|
||||||
});
|
});
|
||||||
|
|
||||||
if scroll_bottom {
|
if scroll_bottom {
|
||||||
ui.scroll_to_cursor(Align::BOTTOM);
|
ui.scroll_to_cursor(Some(Align::BOTTOM));
|
||||||
}
|
}
|
||||||
|
|
||||||
let margin = ui.visuals().clip_rect_margin;
|
let margin = ui.visuals().clip_rect_margin;
|
||||||
|
|
Loading…
Reference in a new issue