Add scroll bars to windows.
Auto-hide scroll bars when not needed Bug fixes: * collapsing headers animation * clip rect interactions * clip rects for scroll areas
This commit is contained in:
parent
2897b1cafc
commit
fbedc2e9ab
12 changed files with 103 additions and 55 deletions
|
@ -13,6 +13,7 @@ This is the core library crate Emigui. It is fully platform independent without
|
|||
* [x] Tooltip
|
||||
* [x] Movable/resizable windows
|
||||
* [ ] Kinetic windows
|
||||
* [ ] BUG FIX: Don't catch clicks on closed windows
|
||||
* [ ] Scroll areas
|
||||
* [x] Vertical scrolling
|
||||
* [ ] Horizontal scrolling
|
||||
|
@ -26,6 +27,10 @@ This is the core library crate Emigui. It is fully platform independent without
|
|||
* [ ] Color picker
|
||||
* [ ] Style editor
|
||||
* [ ] Table with resizable columns
|
||||
* [ ] Layout
|
||||
* [ ] Generalize Layout (separate from Region)
|
||||
* [ ] Cascading layout: same lite if it fits, else next line. Like text.
|
||||
* [ ] Grid layout
|
||||
|
||||
### Web version:
|
||||
* [x] Scroll input
|
||||
|
@ -38,7 +43,7 @@ Add extremely quick animations for some things, maybe 2-3 frames. For instance:
|
|||
### Clip rects
|
||||
* [x] Separate Region::clip_rect from Region::rect
|
||||
* [x] Use clip rectangles when painting
|
||||
* [ ] Use clip rectangles when interacting
|
||||
* [x] Use clip rectangles when interacting
|
||||
* [x] Adjust clip rects so edges of child widgets aren't clipped
|
||||
|
||||
### Modularity
|
||||
|
@ -54,7 +59,6 @@ Add extremely quick animations for some things, maybe 2-3 frames. For instance:
|
|||
* [ ] Text
|
||||
|
||||
### Other
|
||||
* [ ] Generalize Layout so we can create grid layouts etc
|
||||
* [ ] Persist UI state in external storage
|
||||
* [ ] Pixel-perfect rendering (round positions to nearest pixel).
|
||||
* [ ] Build in a profiler which tracks which region in which window takes up CPU.
|
||||
|
|
|
@ -92,16 +92,15 @@ impl CollapsingHeader {
|
|||
|
||||
let animation_time = region.style().animation_time;
|
||||
let time_since_toggle = (region.ctx.input.time - state.toggle_time) as f32;
|
||||
if time_since_toggle < animation_time {
|
||||
let animate = time_since_toggle < animation_time;
|
||||
if animate {
|
||||
region.indent(id, |region| {
|
||||
// animation time
|
||||
|
||||
let max_height = if state.open {
|
||||
remap(
|
||||
time_since_toggle,
|
||||
0.0..=animation_time,
|
||||
// Get instant feedback, and we don't expect to get bigger than this
|
||||
50.0..=1500.0,
|
||||
100.0..=1500.0,
|
||||
)
|
||||
} else {
|
||||
remap_clamp(
|
||||
|
@ -112,17 +111,12 @@ impl CollapsingHeader {
|
|||
)
|
||||
};
|
||||
|
||||
region
|
||||
.clip_rect
|
||||
.set_height(region.clip_rect.height().min(max_height));
|
||||
region.clip_rect.max.y = region.clip_rect.max.y.min(region.cursor.y + max_height);
|
||||
|
||||
add_contents(region);
|
||||
|
||||
region.child_bounds.max.y = region
|
||||
.child_bounds
|
||||
.max
|
||||
.y
|
||||
.min(region.child_bounds.min.y + max_height);
|
||||
region.child_bounds.max.y =
|
||||
region.child_bounds.max.y.min(region.cursor.y + max_height);
|
||||
});
|
||||
} else if state.open {
|
||||
region.indent(id, add_contents);
|
||||
|
|
|
@ -72,7 +72,8 @@ impl Floating {
|
|||
state.size = region.bounding_size().ceil();
|
||||
|
||||
let rect = Rect::from_min_size(state.pos, state.size);
|
||||
let move_interact = ctx.interact(layer, &rect, Some(id.with("move")));
|
||||
let clip_rect = Rect::everything();
|
||||
let move_interact = ctx.interact(layer, &clip_rect, &rect, Some(id.with("move")));
|
||||
|
||||
if move_interact.active {
|
||||
state.pos += ctx.input().mouse_move;
|
||||
|
|
|
@ -172,7 +172,8 @@ impl Resize {
|
|||
contents_region.clip_rect.max = contents_region
|
||||
.clip_rect
|
||||
.max
|
||||
.max(contents_region.clip_rect.min + last_frame_size);
|
||||
.max(contents_region.clip_rect.min + last_frame_size)
|
||||
.min(region.clip_rect.max); // Respect parent region
|
||||
|
||||
add_contents(&mut contents_region);
|
||||
contents_region.bounding_size()
|
||||
|
|
|
@ -4,15 +4,24 @@ use crate::*;
|
|||
pub struct State {
|
||||
/// Positive offset means scrolling down/right
|
||||
pub offset: Vec2,
|
||||
|
||||
pub show_scroll: bool, // TODO: default value?
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScrollArea {
|
||||
max_height: f32,
|
||||
always_show_scroll: bool,
|
||||
auto_hide_scroll: bool,
|
||||
}
|
||||
|
||||
impl Default for ScrollArea {
|
||||
fn default() -> Self {
|
||||
Self { max_height: 200.0 }
|
||||
Self {
|
||||
max_height: 200.0,
|
||||
always_show_scroll: false,
|
||||
auto_hide_scroll: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +30,16 @@ impl ScrollArea {
|
|||
self.max_height = max_height;
|
||||
self
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollArea {
|
||||
|
@ -42,38 +61,40 @@ impl ScrollArea {
|
|||
|
||||
let scroll_bar_width = 16.0;
|
||||
|
||||
let outer_size = vec2(outer_region.available_width(), self.max_height);
|
||||
let outer_size = vec2(
|
||||
outer_region.available_width(),
|
||||
outer_region.available_height().min(self.max_height),
|
||||
);
|
||||
let outer_rect = Rect::from_min_size(outer_region.cursor, outer_size);
|
||||
|
||||
let inner_size = outer_size - vec2(scroll_bar_width, 0.0);
|
||||
let inner_size = if state.show_scroll || !self.auto_hide_scroll {
|
||||
outer_size - vec2(scroll_bar_width, 0.0) // TODO: animate?
|
||||
} else {
|
||||
outer_size
|
||||
};
|
||||
let inner_rect = Rect::from_min_size(outer_region.cursor, inner_size);
|
||||
|
||||
let mut content_region =
|
||||
outer_region.child_region(Rect::from_min_size(outer_region.cursor(), inner_size));
|
||||
content_region.cursor -= state.offset;
|
||||
content_region.desired_rect = content_region.desired_rect.translate(-state.offset);
|
||||
let mut content_region = outer_region.child_region(Rect::from_min_size(
|
||||
outer_region.cursor() - state.offset,
|
||||
vec2(inner_size.x, f32::INFINITY),
|
||||
));
|
||||
content_region.clip_rect = outer_region.clip_rect().intersect(&inner_rect);
|
||||
add_contents(&mut content_region);
|
||||
let content_size = content_region.bounding_size();
|
||||
|
||||
let content_interact = ctx.interact(
|
||||
outer_region.layer,
|
||||
&inner_rect,
|
||||
Some(scroll_area_id.with("area")),
|
||||
);
|
||||
let content_interact = outer_region.interact_rect(&inner_rect, scroll_area_id.with("area"));
|
||||
if content_interact.active {
|
||||
// Dragging scroll area to scroll:
|
||||
state.offset.y -= ctx.input.mouse_move.y;
|
||||
}
|
||||
|
||||
// TODO: check that nothing else is being inteacted with
|
||||
if ctx.contains_mouse_pos(outer_region.layer, &outer_rect)
|
||||
&& ctx.memory.lock().active_id.is_none()
|
||||
{
|
||||
if outer_region.contains_mouse(&outer_rect) && ctx.memory.lock().active_id.is_none() {
|
||||
state.offset.y -= ctx.input.scroll_delta.y;
|
||||
}
|
||||
|
||||
let show_scroll = content_size.y > inner_size.y;
|
||||
if show_scroll {
|
||||
let show_scroll_this_frame = content_size.y > inner_size.y || self.always_show_scroll;
|
||||
if show_scroll_this_frame || state.show_scroll {
|
||||
let left = inner_rect.right() + 2.0;
|
||||
let right = outer_rect.right();
|
||||
let corner_radius = (right - left) / 2.0;
|
||||
|
@ -94,8 +115,8 @@ impl ScrollArea {
|
|||
);
|
||||
|
||||
// intentionally use same id for inside and outside of handle
|
||||
let interact_id = Some(scroll_area_id.with("vertical"));
|
||||
let handle_interact = ctx.interact(outer_region.layer, &handle_rect, interact_id);
|
||||
let interact_id = scroll_area_id.with("vertical");
|
||||
let handle_interact = outer_region.interact_rect(&handle_rect, interact_id);
|
||||
|
||||
if let Some(mouse_pos) = ctx.input.mouse_pos {
|
||||
if handle_interact.active {
|
||||
|
@ -106,7 +127,7 @@ impl ScrollArea {
|
|||
} else {
|
||||
// Check for mouse down outside handle:
|
||||
let scroll_bg_interact =
|
||||
ctx.interact(outer_region.layer, &outer_scroll_rect, interact_id);
|
||||
outer_region.interact_rect(&outer_scroll_rect, interact_id);
|
||||
|
||||
if scroll_bg_interact.active {
|
||||
// Center scroll at mouse pos:
|
||||
|
@ -132,7 +153,7 @@ impl ScrollArea {
|
|||
outer_region.add_paint_cmd(PaintCmd::Rect {
|
||||
rect: outer_scroll_rect,
|
||||
corner_radius,
|
||||
fill_color: Some(color::BLACK),
|
||||
fill_color: Some(color::gray(0, 196)), // TODO style
|
||||
outline: None,
|
||||
});
|
||||
|
||||
|
@ -144,11 +165,12 @@ impl ScrollArea {
|
|||
});
|
||||
}
|
||||
|
||||
let size = content_size.min(content_region.clip_rect.size());
|
||||
let size = content_size.min(inner_rect.size());
|
||||
outer_region.reserve_space_without_padding(size);
|
||||
|
||||
state.offset.y = state.offset.y.max(0.0);
|
||||
state.offset.y = state.offset.y.min(content_size.y - inner_rect.height());
|
||||
state.offset.y = state.offset.y.max(0.0);
|
||||
state.show_scroll = show_scroll_this_frame;
|
||||
|
||||
outer_region
|
||||
.ctx()
|
||||
|
|
|
@ -4,13 +4,14 @@ use crate::{widgets::*, *};
|
|||
|
||||
use super::*;
|
||||
|
||||
// TODO: separate out resizing into a contained and reusable Resize-region.
|
||||
/// A wrapper around other containers for things you often want in a window
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Window {
|
||||
title: String,
|
||||
floating: Floating,
|
||||
frame: Frame,
|
||||
resize: Resize,
|
||||
scroll: ScrollArea,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
@ -22,7 +23,11 @@ impl Window {
|
|||
frame: Frame::default(),
|
||||
resize: Resize::default()
|
||||
.handle_offset(Vec2::splat(4.0))
|
||||
.auto_shrink_height(true),
|
||||
.auto_shrink_height(false)
|
||||
.auto_expand(false),
|
||||
scroll: ScrollArea::default()
|
||||
.always_show_scroll(false)
|
||||
.max_height(f32::INFINITY), // As large as we can be
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,15 +71,17 @@ impl Window {
|
|||
floating,
|
||||
frame,
|
||||
resize,
|
||||
scroll,
|
||||
} = self;
|
||||
// TODO: easier way to compose these
|
||||
floating.show(ctx, |region| {
|
||||
frame.show(region, |region| {
|
||||
resize.show(region, |region| {
|
||||
region.add(Label::new(title).text_style(TextStyle::Heading));
|
||||
region.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents
|
||||
add_contents(region);
|
||||
});
|
||||
});
|
||||
});
|
||||
scroll.show(region, |region| add_contents(region))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,7 +120,8 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn contains_mouse_pos(&self, layer: Layer, rect: &Rect) -> bool {
|
||||
pub fn contains_mouse(&self, layer: Layer, clip_rect: &Rect, rect: &Rect) -> bool {
|
||||
let rect = rect.intersect(clip_rect);
|
||||
if let Some(mouse_pos) = self.input.mouse_pos {
|
||||
rect.contains(mouse_pos) && layer == self.memory.lock().layer_at(mouse_pos)
|
||||
} else {
|
||||
|
@ -128,8 +129,14 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn interact(&self, layer: Layer, rect: &Rect, interaction_id: Option<Id>) -> InteractInfo {
|
||||
let hovered = self.contains_mouse_pos(layer, &rect);
|
||||
pub fn interact(
|
||||
&self,
|
||||
layer: Layer,
|
||||
clip_rect: &Rect,
|
||||
rect: &Rect,
|
||||
interaction_id: Option<Id>,
|
||||
) -> InteractInfo {
|
||||
let hovered = self.contains_mouse(layer, clip_rect, &rect);
|
||||
|
||||
let mut memory = self.memory.lock();
|
||||
let active = interaction_id.is_some() && memory.active_id == interaction_id;
|
||||
|
|
|
@ -108,7 +108,7 @@ impl Emigui {
|
|||
region.input().pixels_per_point,
|
||||
));
|
||||
if let Some(mouse_pos) = region.input().mouse_pos {
|
||||
region.add(label!("mouse_pos: {} x {}", mouse_pos.x, mouse_pos.y,));
|
||||
region.add(label!("mouse_pos: {:.2} x {:.2}", mouse_pos.x, mouse_pos.y,));
|
||||
} else {
|
||||
region.add_label("mouse_pos: None");
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@ impl ExampleApp {
|
|||
Resize::default()
|
||||
.default_height(200.0)
|
||||
// .as_wide_as_possible()
|
||||
.auto_shrink_height(false)
|
||||
.show(region, |region| {
|
||||
region.add(label!("This region can be resized!"));
|
||||
region.add(label!("Just pull the handle on the bottom right"));
|
||||
|
|
|
@ -370,7 +370,7 @@ impl Rect {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn intersect(self, other: Rect) -> Self {
|
||||
pub fn intersect(self, other: &Rect) -> Self {
|
||||
Self {
|
||||
min: self.min.max(other.min),
|
||||
max: self.max.min(other.max),
|
||||
|
|
|
@ -74,7 +74,7 @@ impl Region {
|
|||
pub fn child_region(&self, child_rect: Rect) -> Self {
|
||||
let clip_rect = self
|
||||
.clip_rect
|
||||
.intersect(child_rect.expand(CLIP_RECT_MARGIN));
|
||||
.intersect(&child_rect.expand(CLIP_RECT_MARGIN));
|
||||
Region {
|
||||
ctx: self.ctx.clone(),
|
||||
layer: self.layer,
|
||||
|
@ -339,12 +339,21 @@ impl Region {
|
|||
|
||||
/// Check for clicks on this entire region (desired_rect)
|
||||
pub fn interact_whole(&self) -> InteractInfo {
|
||||
self.ctx
|
||||
.interact(self.layer, &self.desired_rect, Some(self.id))
|
||||
self.ctx.interact(
|
||||
self.layer,
|
||||
&self.clip_rect,
|
||||
&self.desired_rect,
|
||||
Some(self.id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn interact_rect(&self, rect: &Rect, id: Id) -> InteractInfo {
|
||||
self.ctx.interact(self.layer, rect, Some(id))
|
||||
self.ctx
|
||||
.interact(self.layer, &self.clip_rect, rect, Some(id))
|
||||
}
|
||||
|
||||
pub fn contains_mouse(&self, rect: &Rect) -> bool {
|
||||
self.ctx.contains_mouse(self.layer, &self.clip_rect, rect)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
@ -380,7 +389,8 @@ impl Region {
|
|||
};
|
||||
let pos = self.reserve_space_without_padding(padded_size);
|
||||
let rect = Rect::from_min_size(pos, size);
|
||||
self.ctx.interact(self.layer, &rect, interaction_id)
|
||||
self.ctx
|
||||
.interact(self.layer, &self.clip_rect, &rect, interaction_id)
|
||||
}
|
||||
|
||||
/// Reserve this much space and move the cursor.
|
||||
|
|
|
@ -165,5 +165,6 @@ impl Style {
|
|||
region.add(Slider::f32(&mut self.clickable_diameter, 0.0..=60.0).text("clickable_diameter").precision(0));
|
||||
region.add(Slider::f32(&mut self.start_icon_width, 0.0..=60.0).text("start_icon_width").precision(0));
|
||||
region.add(Slider::f32(&mut self.line_width, 0.0..=10.0).text("line_width").precision(0));
|
||||
region.add(Slider::f32(&mut self.animation_time, 0.0..=1.0).text("animation_time").precision(2));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue