2020-04-21 18:52:17 +00:00
|
|
|
use crate::*;
|
|
|
|
|
2020-05-07 08:47:03 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
|
2020-05-02 09:37:12 +00:00
|
|
|
#[serde(default)]
|
|
|
|
pub(crate) struct State {
|
2020-04-22 16:25:02 +00:00
|
|
|
/// Positive offset means scrolling down/right
|
2020-05-02 09:37:12 +00:00
|
|
|
offset: Vec2,
|
2020-04-26 20:30:24 +00:00
|
|
|
|
2020-05-02 09:37:12 +00:00
|
|
|
show_scroll: bool, // TODO: default value?
|
2020-04-22 16:25:02 +00:00
|
|
|
}
|
|
|
|
|
2020-05-01 08:02:53 +00:00
|
|
|
// TODO: rename VScroll
|
2020-04-26 20:30:24 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2020-04-21 18:52:17 +00:00
|
|
|
pub struct ScrollArea {
|
|
|
|
max_height: f32,
|
2020-04-26 20:30:24 +00:00
|
|
|
always_show_scroll: bool,
|
|
|
|
auto_hide_scroll: bool,
|
2020-04-21 18:52:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for ScrollArea {
|
|
|
|
fn default() -> Self {
|
2020-04-26 20:30:24 +00:00
|
|
|
Self {
|
|
|
|
max_height: 200.0,
|
|
|
|
always_show_scroll: false,
|
|
|
|
auto_hide_scroll: true,
|
|
|
|
}
|
2020-04-21 18:52:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ScrollArea {
|
|
|
|
pub fn max_height(mut self, max_height: f32) -> Self {
|
|
|
|
self.max_height = max_height;
|
|
|
|
self
|
|
|
|
}
|
2020-04-26 20:30:24 +00:00
|
|
|
|
|
|
|
pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self {
|
|
|
|
self.always_show_scroll = always_show_scroll;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn auto_hide_scroll(mut self, auto_hide_scroll: bool) -> Self {
|
|
|
|
self.auto_hide_scroll = auto_hide_scroll;
|
|
|
|
self
|
|
|
|
}
|
2020-04-21 18:52:17 +00:00
|
|
|
}
|
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
struct Prepared {
|
|
|
|
id: Id,
|
|
|
|
state: State,
|
|
|
|
current_scroll_bar_width: f32,
|
|
|
|
always_show_scroll: bool,
|
|
|
|
inner_rect: Rect,
|
|
|
|
content_ui: Ui,
|
|
|
|
}
|
|
|
|
|
2020-04-21 18:52:17 +00:00
|
|
|
impl ScrollArea {
|
2020-05-23 09:28:21 +00:00
|
|
|
fn begin(self, ui: &mut Ui) -> Prepared {
|
2020-05-20 16:39:35 +00:00
|
|
|
let Self {
|
|
|
|
max_height,
|
|
|
|
always_show_scroll,
|
|
|
|
auto_hide_scroll,
|
|
|
|
} = self;
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
let ctx = ui.ctx().clone();
|
|
|
|
|
|
|
|
let id = ui.make_child_id("scroll_area");
|
|
|
|
let state = ctx
|
2020-05-02 09:37:12 +00:00
|
|
|
.memory()
|
2020-04-21 18:52:17 +00:00
|
|
|
.scroll_areas
|
2020-05-20 16:39:35 +00:00
|
|
|
.get(&id)
|
2020-04-21 18:52:17 +00:00
|
|
|
.cloned()
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
// content: size of contents (generally large)
|
2020-04-22 21:25:32 +00:00
|
|
|
// outer: size of scroll area including scroll bar(s)
|
|
|
|
// inner: excluding scroll bar(s). The area we clip the contents to.
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-01 08:02:53 +00:00
|
|
|
let max_scroll_bar_width = 16.0;
|
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
let current_scroll_bar_width = if state.show_scroll || !auto_hide_scroll {
|
2020-05-01 08:02:53 +00:00
|
|
|
max_scroll_bar_width // TODO: animate?
|
|
|
|
} else {
|
|
|
|
0.0
|
|
|
|
};
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-04-26 20:30:24 +00:00
|
|
|
let outer_size = vec2(
|
2020-05-20 16:39:35 +00:00
|
|
|
ui.available().width(),
|
|
|
|
ui.available().height().min(max_height),
|
2020-04-26 20:30:24 +00:00
|
|
|
);
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-01 08:02:53 +00:00
|
|
|
let inner_size = outer_size - vec2(current_scroll_bar_width, 0.0);
|
2020-05-20 16:39:35 +00:00
|
|
|
let inner_rect = Rect::from_min_size(ui.available().min, inner_size);
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
let mut content_ui = ui.child_ui(Rect::from_min_size(
|
2020-05-12 20:21:04 +00:00
|
|
|
inner_rect.min - state.offset,
|
2020-04-26 20:30:24 +00:00
|
|
|
vec2(inner_size.x, f32::INFINITY),
|
|
|
|
));
|
2020-05-20 16:39:35 +00:00
|
|
|
let mut content_clip_rect = ui.clip_rect().intersect(inner_rect);
|
|
|
|
content_clip_rect.max.x = ui.clip_rect().max.x - current_scroll_bar_width; // Nice handling of forced resizing beyond the possible
|
2020-05-08 20:42:31 +00:00
|
|
|
content_ui.set_clip_rect(content_clip_rect);
|
2020-05-05 00:32:49 +00:00
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
Prepared {
|
|
|
|
id,
|
|
|
|
state,
|
|
|
|
always_show_scroll,
|
|
|
|
inner_rect,
|
|
|
|
current_scroll_bar_width,
|
|
|
|
content_ui,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
|
2020-05-23 09:28:21 +00:00
|
|
|
let mut prepared = self.begin(ui);
|
2020-05-20 16:39:35 +00:00
|
|
|
let ret = add_contents(&mut prepared.content_ui);
|
2020-05-23 09:28:21 +00:00
|
|
|
prepared.end(ui);
|
2020-05-20 16:39:35 +00:00
|
|
|
ret
|
|
|
|
}
|
2020-05-23 09:28:21 +00:00
|
|
|
}
|
2020-05-20 16:39:35 +00:00
|
|
|
|
2020-05-23 09:28:21 +00:00
|
|
|
impl Prepared {
|
|
|
|
fn end(self, ui: &mut Ui) {
|
2020-05-20 16:39:35 +00:00
|
|
|
let Prepared {
|
|
|
|
id,
|
|
|
|
mut state,
|
|
|
|
inner_rect,
|
|
|
|
always_show_scroll,
|
|
|
|
current_scroll_bar_width,
|
|
|
|
content_ui,
|
2020-05-23 09:28:21 +00:00
|
|
|
} = self;
|
2020-05-20 16:39:35 +00:00
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
let content_size = content_ui.bounding_size();
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-01 08:02:53 +00:00
|
|
|
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(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
let outer_rect = Rect::from_min_size(
|
|
|
|
inner_rect.min,
|
|
|
|
inner_rect.size() + vec2(current_scroll_bar_width, 0.0),
|
|
|
|
);
|
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
let content_is_too_small = content_size.y > inner_rect.height();
|
2020-05-17 14:42:20 +00:00
|
|
|
|
|
|
|
if content_is_too_small {
|
|
|
|
// Dragg contents to scroll (for touch screens mostly):
|
2020-05-20 16:39:35 +00:00
|
|
|
let content_interact = ui.interact_rect(inner_rect, id.with("area"));
|
2020-05-17 14:42:20 +00:00
|
|
|
if content_interact.active {
|
2020-05-21 08:20:16 +00:00
|
|
|
state.offset.y -= ui.input().mouse.delta.y;
|
2020-05-17 14:42:20 +00:00
|
|
|
}
|
2020-04-22 15:38:36 +00:00
|
|
|
}
|
|
|
|
|
2020-04-22 18:01:49 +00:00
|
|
|
// TODO: check that nothing else is being inteacted with
|
2020-05-20 16:39:35 +00:00
|
|
|
if ui.contains_mouse(outer_rect) && ui.memory().active_id.is_none() {
|
|
|
|
state.offset.y -= ui.input().scroll_delta.y;
|
2020-04-22 18:01:49 +00:00
|
|
|
}
|
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
let show_scroll_this_frame = content_is_too_small || always_show_scroll;
|
2020-04-26 20:30:24 +00:00
|
|
|
if show_scroll_this_frame || state.show_scroll {
|
2020-04-22 21:25:32 +00:00
|
|
|
let left = inner_rect.right() + 2.0;
|
2020-04-22 17:38:38 +00:00
|
|
|
let right = outer_rect.right();
|
2020-04-22 21:25:32 +00:00
|
|
|
let corner_radius = (right - left) / 2.0;
|
|
|
|
let top = inner_rect.top();
|
|
|
|
let bottom = inner_rect.bottom();
|
2020-04-21 18:52:17 +00:00
|
|
|
|
|
|
|
let outer_scroll_rect = Rect::from_min_max(
|
2020-04-22 17:38:38 +00:00
|
|
|
pos2(left, inner_rect.top()),
|
|
|
|
pos2(right, inner_rect.bottom()),
|
2020-04-21 18:52:17 +00:00
|
|
|
);
|
|
|
|
|
2020-04-25 09:11:44 +00:00
|
|
|
let from_content =
|
|
|
|
|content_y| remap_clamp(content_y, 0.0..=content_size.y, top..=bottom);
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-04-22 21:25:32 +00:00
|
|
|
let handle_rect = Rect::from_min_max(
|
|
|
|
pos2(left, from_content(state.offset.y)),
|
|
|
|
pos2(right, from_content(state.offset.y + inner_rect.height())),
|
2020-04-21 18:52:17 +00:00
|
|
|
);
|
|
|
|
|
2020-04-22 21:25:32 +00:00
|
|
|
// intentionally use same id for inside and outside of handle
|
2020-05-20 16:39:35 +00:00
|
|
|
let interact_id = id.with("vertical");
|
|
|
|
let handle_interact = ui.interact_rect(handle_rect, interact_id);
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-21 08:20:16 +00:00
|
|
|
if let Some(mouse_pos) = ui.input().mouse.pos {
|
2020-04-22 21:25:32 +00:00
|
|
|
if handle_interact.active {
|
2020-04-22 17:38:38 +00:00
|
|
|
if inner_rect.top() <= mouse_pos.y && mouse_pos.y <= inner_rect.bottom() {
|
2020-04-21 18:52:17 +00:00
|
|
|
state.offset.y +=
|
2020-05-21 08:20:16 +00:00
|
|
|
ui.input().mouse.delta.y * content_size.y / inner_rect.height();
|
2020-04-21 18:52:17 +00:00
|
|
|
}
|
2020-04-22 21:25:32 +00:00
|
|
|
} else {
|
|
|
|
// Check for mouse down outside handle:
|
2020-05-20 16:39:35 +00:00
|
|
|
let scroll_bg_interact = ui.interact_rect(outer_scroll_rect, interact_id);
|
2020-04-22 21:25:32 +00:00
|
|
|
|
|
|
|
if scroll_bg_interact.active {
|
|
|
|
// Center scroll at mouse pos:
|
|
|
|
let mpos_top = mouse_pos.y - handle_rect.height() / 2.0;
|
2020-04-25 09:11:44 +00:00
|
|
|
state.offset.y = remap(mpos_top, top..=bottom, 0.0..=content_size.y);
|
2020-04-22 21:25:32 +00:00
|
|
|
}
|
2020-04-21 18:52:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-22 21:25:32 +00:00
|
|
|
state.offset.y = state.offset.y.max(0.0);
|
|
|
|
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
|
|
|
|
|
|
|
// Avoid frame-delay by calculating a new handle rect:
|
|
|
|
let handle_rect = Rect::from_min_max(
|
|
|
|
pos2(left, from_content(state.offset.y)),
|
|
|
|
pos2(right, from_content(state.offset.y + inner_rect.height())),
|
|
|
|
);
|
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
let style = ui.style();
|
2020-05-10 06:55:41 +00:00
|
|
|
let handle_fill_color = style.interact(&handle_interact).fill_color;
|
2020-05-17 07:44:09 +00:00
|
|
|
let handle_outline = style.interact(&handle_interact).rect_outline;
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
ui.add_paint_cmd(paint::PaintCmd::Rect {
|
2020-04-21 18:52:17 +00:00
|
|
|
rect: outer_scroll_rect,
|
|
|
|
corner_radius,
|
2020-05-20 16:39:35 +00:00
|
|
|
fill_color: Some(ui.style().dark_bg_color),
|
2020-04-21 18:52:17 +00:00
|
|
|
outline: None,
|
|
|
|
});
|
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
ui.add_paint_cmd(paint::PaintCmd::Rect {
|
2020-04-22 21:25:32 +00:00
|
|
|
rect: handle_rect.expand(-2.0),
|
2020-04-21 18:52:17 +00:00
|
|
|
corner_radius,
|
2020-05-17 07:44:09 +00:00
|
|
|
fill_color: Some(handle_fill_color),
|
2020-04-21 18:52:17 +00:00
|
|
|
outline: handle_outline,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-05-01 00:08:01 +00:00
|
|
|
// let size = content_size.min(inner_rect.size());
|
2020-05-01 08:02:53 +00:00
|
|
|
// let size = vec2(
|
|
|
|
// content_size.x, // ignore inner_rect, i.e. try to expand horizontally if necessary
|
|
|
|
// content_size.y.min(inner_rect.size().y), // respect vertical height.
|
|
|
|
// );
|
|
|
|
let size = outer_rect.size();
|
2020-05-20 16:39:35 +00:00
|
|
|
ui.reserve_space(size, None);
|
2020-04-21 18:52:17 +00:00
|
|
|
|
2020-04-22 15:38:36 +00:00
|
|
|
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
2020-04-26 20:30:24 +00:00
|
|
|
state.offset.y = state.offset.y.max(0.0);
|
|
|
|
state.show_scroll = show_scroll_this_frame;
|
2020-04-22 15:38:36 +00:00
|
|
|
|
2020-05-20 16:39:35 +00:00
|
|
|
ui.memory().scroll_areas.insert(id, state);
|
2020-04-21 18:52:17 +00:00
|
|
|
}
|
|
|
|
}
|