From 88bfcd585e52ad6ad06f0011c94bd067083378ef Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 17 May 2020 16:42:20 +0200 Subject: [PATCH] Resize windows by dragging any side or corner --- docs/index.html | 4 + emigui/README.md | 2 +- emigui/src/containers/area.rs | 12 + emigui/src/containers/collapsing_header.rs | 8 + emigui/src/containers/frame.rs | 3 +- emigui/src/containers/resize.rs | 37 ++- emigui/src/containers/scroll_area.rs | 14 +- emigui/src/containers/window.rs | 255 ++++++++++++++++++++- emigui/src/context.rs | 11 +- emigui/src/math.rs | 12 + emigui/src/memory.rs | 5 +- emigui/src/types.rs | 3 + emigui_glium/src/lib.rs | 3 + 13 files changed, 353 insertions(+), 16 deletions(-) diff --git a/docs/index.html b/docs/index.html index a207cde9..f886b101 100644 --- a/docs/index.html +++ b/docs/index.html @@ -63,6 +63,7 @@ let output = JSON.parse(wasm_bindgen.run_gui(g_wasm_app, JSON.stringify(input))); // console.log(`output: ${JSON.stringify(output)}`); document.body.style.cursor = from_emigui_cursor(output.cursor_icon); + // console.log(`Translated ${output.cursor_icon} to ${document.body.style.cursor}`); if (output.open_url) { window.open(output.open_url, "_self"); } @@ -71,7 +72,10 @@ function from_emigui_cursor(cursor) { if (cursor == "no_drop") { return "no-drop"; } else if (cursor == "not_allowed") { return "not-allowed"; } + else if (cursor == "resize_horizontal") { return "ew-resize"; } + else if (cursor == "resize_ne_sw") { return "nesw-resize"; } else if (cursor == "resize_nw_se") { return "nwse-resize"; } + else if (cursor == "resize_vertical") { return "ns-resize"; } else if (cursor == "pointing_hand") { return "pointer"; } // TODO: more else { diff --git a/emigui/README.md b/emigui/README.md index fb5b1aae..0c05582c 100644 --- a/emigui/README.md +++ b/emigui/README.md @@ -15,7 +15,7 @@ This is the core library crate Emigui. It is fully platform independent without * [x] Kinetic windows * [ ] Windows should open from `UI`s and be boxed by parent ui. * Then we could open the example app inside a window in the example app, recursively. - * [ ] Resize any side and corner on windows + * [x] Resize any side and corner on windows * [ ] Fix autoshrink * [ ] Scroll areas * [x] Vertical scrolling diff --git a/emigui/src/containers/area.rs b/emigui/src/containers/area.rs index 5470a937..3ffb3b9a 100644 --- a/emigui/src/containers/area.rs +++ b/emigui/src/containers/area.rs @@ -46,12 +46,24 @@ impl Area { } } + pub fn layer(&self) -> Layer { + Layer { + order: self.order, + id: self.id, + } + } + + /// moveable by draggin the area? pub fn movable(mut self, movable: bool) -> Self { self.movable = movable; self.interactable |= movable; self } + pub fn is_movable(&self) -> bool { + self.movable + } + /// If false, clicks goes stright throught to what is behind us. /// Good for tooltips etc. pub fn interactable(mut self, interactable: bool) -> Self { diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 4791ac41..682c8c06 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -34,6 +34,14 @@ impl State { .clone() } + // Helper + pub fn is_open(ctx: &Context, id: Id) -> Option { + ctx.memory() + .collapsing_headers + .get(&id) + .map(|state| state.open) + } + pub fn toggle(&mut self, ui: &Ui) { self.open = !self.open; self.toggle_time = ui.input().time; diff --git a/emigui/src/containers/frame.rs b/emigui/src/containers/frame.rs index 76c0ce75..2c5630bc 100644 --- a/emigui/src/containers/frame.rs +++ b/emigui/src/containers/frame.rs @@ -4,6 +4,7 @@ use crate::*; #[derive(Clone, Debug, Default)] pub struct Frame { + // On each side pub margin: Vec2, pub corner_radius: f32, pub fill_color: Option, @@ -68,7 +69,7 @@ impl Frame { } = self; let outer_rect = ui.available(); - let inner_rect = outer_rect.expand2(-margin); + let inner_rect = outer_rect.shrink2(margin); let where_to_put_background = ui.paint_list_len(); let mut child_ui = ui.child_ui(inner_rect); diff --git a/emigui/src/containers/resize.rs b/emigui/src/containers/resize.rs index 64aa5385..675ce74f 100644 --- a/emigui/src/containers/resize.rs +++ b/emigui/src/containers/resize.rs @@ -4,12 +4,17 @@ use crate::*; #[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)] pub(crate) struct State { - size: Vec2, + pub(crate) size: Vec2, + + /// Externally requested size (e.g. by Window) for the next frame + pub(crate) requested_size: Option, } // TODO: auto-shink/grow should be part of another container! #[derive(Clone, Copy, Debug)] pub struct Resize { + id: Option, + /// If false, we are no enabled resizable: bool, @@ -34,6 +39,7 @@ pub struct Resize { impl Default for Resize { fn default() -> Self { Self { + id: None, resizable: true, min_size: Vec2::splat(16.0), max_size: Vec2::infinity(), @@ -49,6 +55,12 @@ impl Default for Resize { } impl Resize { + /// Assign an explicit and globablly unique id. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + pub fn default_width(mut self, width: f32) -> Self { self.default_size.x = width; self @@ -81,6 +93,10 @@ impl Resize { self } + pub fn is_resizable(&self) -> bool { + self.resizable + } + /// Not resizable, just takes the size of its contents. pub fn auto_sized(self) -> Self { self.default_size(Vec2::splat(f32::INFINITY)) @@ -152,10 +168,9 @@ impl Resize { } } -// TODO: a common trait for Things that follow this pattern impl Resize { pub fn show(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) { - let id = ui.make_child_id("resize"); + let id = self.id.unwrap_or_else(|| ui.make_child_id("resize")); self.min_size = self.min_size.min(ui.available().size()); self.max_size = self.max_size.min(ui.available().size()); self.max_size = self.max_size.max(self.min_size); @@ -164,7 +179,13 @@ impl Resize { Some(state) => (false, *state), None => { let default_size = self.default_size.clamp(self.min_size..=self.max_size); - (true, State { size: default_size }) + ( + true, + State { + size: default_size, + requested_size: None, + }, + ) } }; @@ -199,6 +220,14 @@ impl Resize { None }; + if let Some(requested_size) = state.requested_size.take() { + state.size = requested_size; + // We don't clamp to max size, because we want to be able to push against outer bounds. + // For instance, if we are inside a bigger Resize region, we want to expand that. + // state.size = state.size.clamp(self.min_size..=self.max_size); + state.size = state.size.max(self.min_size); + } + // ------------------------------ let inner_rect = Rect::from_min_size(position, state.size); diff --git a/emigui/src/containers/scroll_area.rs b/emigui/src/containers/scroll_area.rs index 9078f0a6..6b798b66 100644 --- a/emigui/src/containers/scroll_area.rs +++ b/emigui/src/containers/scroll_area.rs @@ -100,10 +100,14 @@ impl ScrollArea { inner_rect.size() + vec2(current_scroll_bar_width, 0.0), ); - let content_interact = outer_ui.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; + let content_is_too_small = content_size.y > inner_size.y; + + if content_is_too_small { + // Dragg contents to scroll (for touch screens mostly): + let content_interact = outer_ui.interact_rect(inner_rect, scroll_area_id.with("area")); + if content_interact.active { + state.offset.y -= ctx.input().mouse_move.y; + } } // TODO: check that nothing else is being inteacted with @@ -111,7 +115,7 @@ impl ScrollArea { state.offset.y -= ctx.input().scroll_delta.y; } - let show_scroll_this_frame = content_size.y > inner_size.y || self.always_show_scroll; + let show_scroll_this_frame = content_is_too_small || self.always_show_scroll; if show_scroll_this_frame || state.show_scroll { let left = inner_rect.right() + 2.0; let right = outer_rect.right(); diff --git a/emigui/src/containers/window.rs b/emigui/src/containers/window.rs index a5f7df8b..27da081d 100644 --- a/emigui/src/containers/window.rs +++ b/emigui/src/containers/window.rs @@ -136,15 +136,26 @@ impl<'open> Window<'open> { scroll, } = self; + let movable = area.is_movable(); + let area = area.movable(false); // We move it manually + let resizable = resize.is_resizable(); + let resize = resize.resizable(false); // We move it manually + + let window_id = Id::new(title_label.text()); + let area_layer = area.layer(); + let resize_id = window_id.with("resize"); + let collapsing_id = window_id.with("collapsing"); + + let resize = resize.id(resize_id); + if matches!(open, Some(false)) { return None; } let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); - Some(area.show(ctx, |ui| { + let full_interact = area.show(ctx, |ui| { frame.show(ui, |ui| { - let collapsing_id = ui.make_child_id("collapsing"); let default_expanded = true; let mut collapsing = collapsing_header::State::from_memory_with_default_open( ui, @@ -163,6 +174,7 @@ impl<'open> Window<'open> { .collapsing_headers .insert(collapsing_id, collapsing); + // TODO: fix collapsing window animation let content = collapsing.add_contents(ui, |ui| { resize.show(ui, |ui| { ui.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents @@ -195,10 +207,247 @@ impl<'open> Window<'open> { } } }) - })) + }); + + let resizable = + resizable && collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default(); + + if movable || resizable { + let possible = PossibleInteractions { movable, resizable }; + + // TODO: not when collapsed, and not when resizing or moving is disabled etc + let pre_resize = ctx.round_rect_to_pixels(full_interact.rect); + let new_rect = resize_window( + ctx, + possible, + area_layer, + window_id.with("frame_resize"), + pre_resize, + ); + let new_rect = ctx.round_rect_to_pixels(new_rect); + if new_rect != pre_resize { + let mut area_state = ctx.memory().areas.get(area_layer.id).unwrap(); + area_state.pos = new_rect.min; + ctx.memory().areas.set_state(area_layer, area_state); + + let mut resize_state = ctx.memory().resize.get(&resize_id).cloned().unwrap(); + // resize_state.size += new_rect.size() - pre_resize.size(); + // resize_state.size = new_rect.size() - some margin; + resize_state.requested_size = + Some(resize_state.size + new_rect.size() - pre_resize.size()); + ctx.memory().resize.insert(resize_id, resize_state); + + ctx.memory().areas.move_to_top(area_layer); + } + } + + Some(full_interact) } } +// ---------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug)] +struct PossibleInteractions { + movable: bool, + resizable: bool, +} + +#[derive(Clone, Copy, Debug)] +pub struct FrameInteraction { + area_layer: Layer, + start_rect: Rect, + start_mouse_pos: Pos2, + left: bool, + right: bool, + top: bool, + bottom: bool, +} + +impl FrameInteraction { + pub fn set_cursor(&self, ctx: &Context) { + if (self.left && self.top) || (self.right && self.bottom) { + ctx.output().cursor_icon = CursorIcon::ResizeNwSe; + } else if (self.right && self.top) || (self.left && self.bottom) { + ctx.output().cursor_icon = CursorIcon::ResizeNeSw; + } else if self.left || self.right { + ctx.output().cursor_icon = CursorIcon::ResizeHorizontal; + } else if self.bottom || self.top { + ctx.output().cursor_icon = CursorIcon::ResizeVertical; + } + } + + pub fn is_resize(&self) -> bool { + self.left || self.right || self.top || self.bottom + } + + pub fn is_pure_move(&self) -> bool { + !self.is_resize() + } +} + +fn resize_window( + ctx: &Context, + possible: PossibleInteractions, + area_layer: Layer, + id: Id, + mut rect: Rect, +) -> Rect { + if let Some(frame_interaction) = frame_interaction(ctx, possible, area_layer, id, rect) { + frame_interaction.set_cursor(ctx); + if let Some(mouse_pos) = ctx.input().mouse_pos { + rect = frame_interaction.start_rect; // prevent drift + + if frame_interaction.is_resize() { + if frame_interaction.left { + rect.min.x = ctx.round_to_pixel(mouse_pos.x); + } else if frame_interaction.right { + rect.max.x = ctx.round_to_pixel(mouse_pos.x); + } + + if frame_interaction.top { + rect.min.y = ctx.round_to_pixel(mouse_pos.y); + } else if frame_interaction.bottom { + rect.max.y = ctx.round_to_pixel(mouse_pos.y); + } + } else { + // movevement + rect = rect.translate(mouse_pos - frame_interaction.start_mouse_pos); + } + } + } + + return rect; +} + +fn frame_interaction( + ctx: &Context, + possible: PossibleInteractions, + area_layer: Layer, + id: Id, + rect: Rect, +) -> Option { + { + let active_id = ctx.memory().active_id; + if active_id.is_none() { + let frame_interaction = ctx.memory().frame_interaction; + if let Some(frame_interaction) = frame_interaction { + if frame_interaction.area_layer == area_layer { + eprintln!("Letting go of window"); + if frame_interaction.is_pure_move() { + // Throw window: + let mut area_state = ctx.memory().areas.get(area_layer.id).unwrap(); + area_state.vel = ctx.input().mouse_velocity; + eprintln!("Throwing window with velocity {:?}", area_state.vel); + ctx.memory().areas.set_state(area_layer, area_state); + } + ctx.memory().frame_interaction = None; + } + } + } + + if active_id.is_some() && active_id != Some(id) { + return None; + } + } + + let mut frame_interaction = { ctx.memory().frame_interaction.clone() }; + + if frame_interaction.is_none() { + if let Some(hover_frame_interaction) = resize_hover(ctx, possible, area_layer, rect) { + hover_frame_interaction.set_cursor(ctx); + if ctx.input().mouse_pressed { + ctx.memory().active_id = Some(id); + frame_interaction = Some(hover_frame_interaction); + ctx.memory().frame_interaction = frame_interaction; + } + } + } + + if let Some(frame_interaction) = frame_interaction { + let is_active = ctx.memory().active_id == Some(id); + + if is_active && frame_interaction.area_layer == area_layer { + return Some(frame_interaction); + } + } + + None +} + +fn resize_hover( + ctx: &Context, + possible: PossibleInteractions, + area_layer: Layer, + rect: Rect, +) -> Option { + if let Some(mouse_pos) = ctx.input().mouse_pos { + if let Some(top_layer) = ctx.memory().layer_at(mouse_pos) { + if top_layer != area_layer && top_layer.order != Order::Background { + return None; // Another window is on top here + } + } + + let side_interact_radius = 5.0; // TODO: from style + let corner_interact_radius = 10.0; // TODO + if rect.expand(side_interact_radius).contains(mouse_pos) { + let (mut left, mut right, mut top, mut bottom) = Default::default(); + if possible.resizable { + right = (rect.right() - mouse_pos.x).abs() <= side_interact_radius; + bottom = (rect.bottom() - mouse_pos.y).abs() <= side_interact_radius; + + if rect.right_bottom().dist(mouse_pos) < corner_interact_radius { + right = true; + bottom = true; + } + + if possible.movable { + left = (rect.left() - mouse_pos.x).abs() <= side_interact_radius; + top = (rect.top() - mouse_pos.y).abs() <= side_interact_radius; + + if rect.right_top().dist(mouse_pos) < corner_interact_radius { + right = true; + top = true; + } + if rect.left_top().dist(mouse_pos) < corner_interact_radius { + left = true; + top = true; + } + if rect.left_bottom().dist(mouse_pos) < corner_interact_radius { + left = true; + bottom = true; + } + } + } + let any_resize = left || right || top || bottom; + + if !any_resize && !possible.movable { + return None; + } + + if any_resize || possible.movable { + Some(FrameInteraction { + area_layer, + start_rect: rect, + start_mouse_pos: mouse_pos, + left, + right, + top, + bottom, + }) + } else { + None + } + } else { + None + } + } else { + None + } +} + +// ---------------------------------------------------------------------------- + fn show_title_bar( ui: &mut Ui, title_label: Label, diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 244daf38..c902da8c 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -151,6 +151,13 @@ impl Context { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } + pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect { + Rect { + min: self.round_pos_to_pixels(rect.min), + max: self.round_pos_to_pixels(rect.max), + } + } + // --------------------------------------------------------------------- pub fn begin_frame(self: &mut Arc, new_input: RawInput) { @@ -173,7 +180,9 @@ impl Context { if let Some(mouse_pos) = new_raw_input.mouse_pos { self.mouse_tracker.add(new_raw_input.time, mouse_pos); } else { - self.mouse_tracker.clear(); + // we do not clear the `mouse_tracker` here, because it is exactly when a finger has + // released from the touch screen that we may want to assign a velocity to whatever + // the user tried to throw } let new_input = GuiInput::from_last_and_new(&self.raw_input, &new_raw_input); self.previus_input = std::mem::replace(&mut self.input, new_input); diff --git a/emigui/src/math.rs b/emigui/src/math.rs index ece314f0..434c5927 100644 --- a/emigui/src/math.rs +++ b/emigui/src/math.rs @@ -391,6 +391,18 @@ impl Rect { Rect::from_min_max(self.min - amnt, self.max + amnt) } + /// Shrink by this much in each direction, keeping the center + #[must_use] + pub fn shrink(self, amnt: f32) -> Self { + self.shrink2(Vec2::splat(amnt)) + } + + /// Shrink by this much in each direction, keeping the center + #[must_use] + pub fn shrink2(self, amnt: Vec2) -> Self { + Rect::from_min_max(self.min + amnt, self.max - amnt) + } + #[must_use] pub fn translate(self, amnt: Vec2) -> Self { Rect::from_min_size(self.min + amnt, self.size()) diff --git a/emigui/src/memory.rs b/emigui/src/memory.rs index 7090bad7..82b2a16e 100644 --- a/emigui/src/memory.rs +++ b/emigui/src/memory.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ - containers::{area, collapsing_header, menu, resize, scroll_area}, + containers::{area, collapsing_header, menu, resize, scroll_area, window}, widgets::text_edit, Id, Layer, Pos2, Rect, }; @@ -24,6 +24,9 @@ pub struct Memory { pub(crate) scroll_areas: HashMap, pub(crate) text_edit: HashMap, + #[serde(skip)] + pub(crate) frame_interaction: Option, + pub(crate) areas: Areas, } diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 41325e44..98e05824 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -30,7 +30,10 @@ pub enum CursorIcon { Default, /// Pointing hand, used for e.g. web links PointingHand, + ResizeHorizontal, + ResizeNeSw, ResizeNwSe, + ResizeVertical, Text, } diff --git a/emigui_glium/src/lib.rs b/emigui_glium/src/lib.rs index 37eeccee..32ac3d9c 100644 --- a/emigui_glium/src/lib.rs +++ b/emigui_glium/src/lib.rs @@ -164,7 +164,10 @@ pub fn translate_cursor(cursor_icon: emigui::CursorIcon) -> glutin::MouseCursor match cursor_icon { CursorIcon::Default => glutin::MouseCursor::Default, CursorIcon::PointingHand => glutin::MouseCursor::Hand, + CursorIcon::ResizeHorizontal => glutin::MouseCursor::EwResize, + CursorIcon::ResizeNeSw => glutin::MouseCursor::NeswResize, CursorIcon::ResizeNwSe => glutin::MouseCursor::NwseResize, + CursorIcon::ResizeVertical => glutin::MouseCursor::NsResize, CursorIcon::Text => glutin::MouseCursor::Text, } }