From fbedc2e9ab507314cf2d65592d88b094f1bc99cf Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 26 Apr 2020 22:30:24 +0200 Subject: [PATCH] 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 --- emigui/README.md | 8 ++- emigui/src/containers/collapsing_header.rs | 18 ++---- emigui/src/containers/floating.rs | 3 +- emigui/src/containers/resize.rs | 3 +- emigui/src/containers/scroll_area.rs | 68 ++++++++++++++-------- emigui/src/containers/window.rs | 19 ++++-- emigui/src/context.rs | 13 ++++- emigui/src/emigui.rs | 2 +- emigui/src/example_app.rs | 1 + emigui/src/math.rs | 2 +- emigui/src/region.rs | 20 +++++-- emigui/src/style.rs | 1 + 12 files changed, 103 insertions(+), 55 deletions(-) diff --git a/emigui/README.md b/emigui/README.md index 7c67167f..99a70280 100644 --- a/emigui/README.md +++ b/emigui/README.md @@ -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. diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 3fe901fd..97c9b4be 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -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); diff --git a/emigui/src/containers/floating.rs b/emigui/src/containers/floating.rs index aec7ffdc..16575212 100644 --- a/emigui/src/containers/floating.rs +++ b/emigui/src/containers/floating.rs @@ -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; diff --git a/emigui/src/containers/resize.rs b/emigui/src/containers/resize.rs index bb73c18a..bbcc79d7 100644 --- a/emigui/src/containers/resize.rs +++ b/emigui/src/containers/resize.rs @@ -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() diff --git a/emigui/src/containers/scroll_area.rs b/emigui/src/containers/scroll_area.rs index f9add704..d72c6292 100644 --- a/emigui/src/containers/scroll_area.rs +++ b/emigui/src/containers/scroll_area.rs @@ -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() diff --git a/emigui/src/containers/window.rs b/emigui/src/containers/window.rs index 703fa01e..9ef12126 100644 --- a/emigui/src/containers/window.rs +++ b/emigui/src/containers/window.rs @@ -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)) + }) + }) + }) } } diff --git a/emigui/src/context.rs b/emigui/src/context.rs index c66ae1dc..924f3edb 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -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) -> InteractInfo { - let hovered = self.contains_mouse_pos(layer, &rect); + pub fn interact( + &self, + layer: Layer, + clip_rect: &Rect, + rect: &Rect, + interaction_id: Option, + ) -> 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; diff --git a/emigui/src/emigui.rs b/emigui/src/emigui.rs index f551b243..438d886e 100644 --- a/emigui/src/emigui.rs +++ b/emigui/src/emigui.rs @@ -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"); } diff --git a/emigui/src/example_app.rs b/emigui/src/example_app.rs index 354f6d6b..d1151ff8 100644 --- a/emigui/src/example_app.rs +++ b/emigui/src/example_app.rs @@ -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")); diff --git a/emigui/src/math.rs b/emigui/src/math.rs index 9e83ebb0..468c8784 100644 --- a/emigui/src/math.rs +++ b/emigui/src/math.rs @@ -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), diff --git a/emigui/src/region.rs b/emigui/src/region.rs index 3aac818e..fbc6ca85 100644 --- a/emigui/src/region.rs +++ b/emigui/src/region.rs @@ -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. diff --git a/emigui/src/style.rs b/emigui/src/style.rs index 7598f983..660a3346 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -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)); } }