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,
}
}