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

Add manual scrolling #81

Merged
merged 18 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from 16 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
39 changes: 38 additions & 1 deletion egui/src/containers/scroll_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub struct ScrollArea {
max_height: f32,
always_show_scroll: bool,
id_source: Option<Id>,
offset: Option<Vec2>,
}

impl ScrollArea {
Expand All @@ -45,6 +46,7 @@ impl ScrollArea {
max_height,
always_show_scroll: false,
id_source: None,
offset: None,
}
}

Expand All @@ -60,6 +62,15 @@ impl ScrollArea {
self.id_source = Some(Id::new(id_source));
self
}

/// Set the scroll offset position.
///
/// See also: [`Ui::scroll_to_here`](crate::ui::Ui::scroll_to_here) and
/// [`Response::scroll_to_me`](crate::types::Response::scroll_to_me)
pub fn scroll_offset(mut self, offset: Vec2) -> Self {
lucaspoffo marked this conversation as resolved.
Show resolved Hide resolved
self.offset = Some(offset);
self
}
}

struct Prepared {
Expand All @@ -77,19 +88,24 @@ impl ScrollArea {
max_height,
always_show_scroll,
id_source,
offset,
} = self;

let ctx = ui.ctx().clone();

let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_source);
let state = ctx
let mut state = ctx
.memory()
.scroll_areas
.get(&id)
.cloned()
.unwrap_or_default();

if let Some(offset) = offset {
state.offset = offset;
}

// content: size of contents (generally large; that's why we want scroll bars)
// outer: size of scroll area including scroll bar(s)
// inner: excluding scroll bar(s). The area we clip the contents to.
Expand Down Expand Up @@ -155,6 +171,27 @@ impl Prepared {

let content_size = content_ui.min_size();

let scroll_target = content_ui.ctx().frame_state().scroll_target();
if let Some(scroll_target) = scroll_target {
let center_ratio = content_ui.ctx().frame_state().scroll_target_center_ratio();
let height_offset = content_ui.clip_rect().height() * center_ratio;
let top = content_ui.min_rect().top();
let offset_y = scroll_target - top - height_offset;
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
let height_offset = content_ui.clip_rect().height() * center_ratio;
let top = content_ui.min_rect().top();
let offset_y = scroll_target - top - height_offset;
let offset_y = scroll_target - lerp(content_ui.clip_rect().y_range(), center_ratio);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This does not work, as the clip_rect is always the same, but I updated the code to be more easy to understand. I hope.

state.offset.y = offset_y;

// We need to clear the offset
// or else all the ScrollAreas will use this offset
content_ui.ctx().frame_state().set_scroll_target(None);
lucaspoffo marked this conversation as resolved.
Show resolved Hide resolved
}

let inner_rect = Rect::from_min_size(
inner_rect.min,
vec2(
inner_rect.width().max(content_size.x), // Expand width to fit content
inner_rect.height(),
),
);

lucaspoffo marked this conversation as resolved.
Show resolved Hide resolved
let width = if inner_rect.width().is_finite() {
inner_rect.width().max(content_size.x) // Expand width to fit content
} else {
Expand Down
25 changes: 25 additions & 0 deletions egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub(crate) struct FrameState {

/// How much space is used by panels.
used_by_panels: Rect,
scroll_target: Option<f32>,
scroll_target_center_factor: f32,
lucaspoffo marked this conversation as resolved.
Show resolved Hide resolved
lucaspoffo marked this conversation as resolved.
Show resolved Hide resolved
// TODO: move some things from `Memory` to here
}

Expand All @@ -47,6 +49,8 @@ impl Default for FrameState {
available_rect: Rect::invalid(),
unused_rect: Rect::invalid(),
used_by_panels: Rect::invalid(),
scroll_target: None,
scroll_target_center_factor: 0.0,
}
}
}
Expand All @@ -56,6 +60,8 @@ impl FrameState {
self.available_rect = input.screen_rect();
self.unused_rect = input.screen_rect();
self.used_by_panels = Rect::nothing();
self.scroll_target = None;
self.scroll_target_center_factor = 0.0;
}

/// How much space is still available after panels has been added.
Expand Down Expand Up @@ -97,6 +103,25 @@ impl FrameState {
self.unused_rect = Rect::nothing(); // Nothing left unused after this
self.used_by_panels = self.used_by_panels.union(panel_rect);
}

pub(crate) fn set_scroll_target(&mut self, scroll_target: Option<f32>) {
self.scroll_target = scroll_target;
}

pub(crate) fn scroll_target(&self) -> Option<f32> {
self.scroll_target
}

pub(crate) fn set_scroll_target_center_factor(&mut self, center_factor: f32) {
// This is the imgui aproach to set the scroll relative to the position
// 0.0: top of widget / 0.5: midle of widget / 1.0: bottom of widget
assert!(center_factor >= 0.0 && center_factor <= 1.0);
self.scroll_target_center_factor = center_factor;
}

pub(crate) fn scroll_target_center_ratio(&self) -> f32 {
self.scroll_target_center_factor
}
}

// ----------------------------------------------------------------------------
Expand Down
6 changes: 3 additions & 3 deletions egui/src/demos/demo_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub struct DemoWindow {
num_columns: usize,

widgets: Widgets,
scrolls: Scrolls,
colors: ColorWidgets,
layout: LayoutDemo,
tree: Tree,
Expand All @@ -18,6 +19,7 @@ impl Default for DemoWindow {
DemoWindow {
num_columns: 2,

scrolls: Default::default(),
widgets: Default::default(),
colors: Default::default(),
layout: Default::default(),
Expand Down Expand Up @@ -68,9 +70,7 @@ impl DemoWindow {
CollapsingHeader::new("Scroll area")
.default_open(false)
.show(ui, |ui| {
ScrollArea::from_max_height(200.0).show(ui, |ui| {
ui.label(LOREM_IPSUM_LONG);
});
lucaspoffo marked this conversation as resolved.
Show resolved Hide resolved
self.scrolls.ui(ui);
});

CollapsingHeader::new("Resize")
Expand Down
3 changes: 2 additions & 1 deletion egui/src/demos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod font_contents_emoji;
pub mod font_contents_ubuntu;
mod fractal_clock;
mod painting;
mod scrolls;
mod sliders;
mod tests;
pub mod toggle_switch;
Expand All @@ -20,7 +21,7 @@ mod widgets;
pub use {
app::*, color_test::ColorTest, dancing_strings::DancingStrings, demo_window::DemoWindow,
demo_windows::*, drag_and_drop::*, font_book::FontBook, fractal_clock::FractalClock,
painting::Painting, sliders::Sliders, tests::Tests, widgets::Widgets,
painting::Painting, scrolls::Scrolls, sliders::Sliders, tests::Tests, widgets::Widgets,
};

pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
Expand Down
74 changes: 74 additions & 0 deletions egui/src/demos/scrolls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use crate::{color::*, *};

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Scrolls {
track_item: usize,
tracking: bool,
offset: f32,
center_factor: f32,
}

impl Default for Scrolls {
fn default() -> Self {
Self {
track_item: 25,
tracking: true,
offset: 0.0,
center_factor: 0.3,
}
}
}

impl Scrolls {
pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.checkbox(&mut self.tracking, "Track")
.on_hover_text("The scroll position will track the selected item");
ui.add(Slider::usize(&mut self.track_item, 1..=50).text("Track Item"));
});
ui.add(Slider::f32(&mut self.center_factor, 0.0..=1.0).text("Custom scroll center factor"));
let (scroll_offset, _) = ui.horizontal(|ui| {
let scroll_offset = ui.small_button("Scroll Offset").clicked;
ui.add(DragValue::f32(&mut self.offset).speed(1.0).suffix("px"));
scroll_offset
});

const TITLES: [&str; 6] = ["Top", "25%", "Middle", "75%", "Bottom", "Custom"];
ui.columns(6, |cols| {
for (i, col) in cols.iter_mut().enumerate() {
col.colored_label(WHITE, TITLES[i]);
let mut scroll_area = ScrollArea::from_max_height(200.0).id_source(i);
if scroll_offset {
self.tracking = false;
scroll_area = scroll_area.scroll_offset(Vec2::new(0.0, self.offset));
}

let (current_scroll, max_scroll) = scroll_area.show(col, |ui| {
ui.vertical(|ui| {
for item in 1..=50 {
if self.tracking && item == self.track_item {
let response = ui.colored_label(YELLOW, format!("Item {}", item));
let scroll_center_factor = if i == 5 {
self.center_factor
} else {
0.25 * i as f32
};
response.scroll_to_me(scroll_center_factor);
} else {
ui.label(format!("Item {}", item));
}
}
});

let margin = ui.style().visuals.clip_rect_margin;
(
ui.clip_rect().top() - ui.min_rect().top() + margin,
ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin,
)
});
col.colored_label(WHITE, format!("{:.0}/{:.0}", current_scroll, max_scroll));
}
});
}
}
15 changes: 14 additions & 1 deletion egui/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{math::Rect, CtxRef, Id, LayerId, Ui};
use crate::{lerp, math::Rect, CtxRef, Id, LayerId, Ui};

// ----------------------------------------------------------------------------

Expand Down Expand Up @@ -159,6 +159,19 @@ impl Response {
self.ctx
.interact_with_hovered(self.layer_id, self.id, self.rect, sense, self.hovered)
}

/// Move the scroll to this UI.
/// The scroll centering is based on the `center_factor`:
/// * 0.0 - top
/// * 0.5 - middle
/// * 1.0 - bottom
pub fn scroll_to_me(&self, center_factor: f32) {
emilk marked this conversation as resolved.
Show resolved Hide resolved
let scroll_target = lerp(self.rect.y_range(), center_factor);

let mut frame_state = self.ctx.frame_state();
frame_state.set_scroll_target(Some(scroll_target));
frame_state.set_scroll_target_center_factor(center_factor);
}
}

impl Response {
Expand Down
13 changes: 13 additions & 0 deletions egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,19 @@ impl Ui {
let painter = Painter::new(self.ctx().clone(), self.layer_id(), clip_rect);
(response, painter)
}

/// Move the scroll to this position.
/// The scroll centering is based on the `center_factor`:
/// * 0.0 - top
/// * 0.5 - middle
/// * 1.0 - bottom
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
/// * 1.0 - bottom
/// * 1.0 - bottom
///
/// ```
/// # let mut ui = egui::Ui::__test();
/// egui::ScrollArea::auto_sized().show(ui, |ui| {
/// for i in 0..1000 {
/// if ui.button(format!("Button {}", i)).clicked {
/// ui.scroll_to_here(0.5);
/// }
/// }
/// });
/// ```

Copy link
Owner

Choose a reason for hiding this comment

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

...though in this case, this would make more sense:

let response = ui.button(format!("Button {}", i));
if response.clicked {
    response.scroll_to_me(0.5);
}

Is there any other more plausible use case of this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the code example from the scroll_to_cursor to use the scroll_bottom example similar from the demo. Makes more sense for this case. Added the button example for the scroll_to_me.

pub fn scroll_to_here(&mut self, center_factor: f32) {
let scroll_target = self.min_rect().bottom();
Copy link
Owner

Choose a reason for hiding this comment

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

This will scroll to the bottom of the current size of the Ui, not to the current cursor position (a Ui can be resized to something large before adding elements to it).

I think scroll_to_cursor and let scroll_target = self.region.cursor.y; is better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point 👍 Didn't knew about that.


let mut frame_state = self.ctx().frame_state();
frame_state.set_scroll_target(Some(scroll_target));
frame_state.set_scroll_target_center_factor(center_factor);
}
}

/// # Adding widgets
Expand Down