Resize windows by dragging any side or corner

This commit is contained in:
Emil Ernerfeldt 2020-05-17 16:42:20 +02:00
parent f9bb9f71c4
commit 88bfcd585e
13 changed files with 353 additions and 16 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -34,6 +34,14 @@ impl State {
.clone()
}
// Helper
pub fn is_open(ctx: &Context, id: Id) -> Option<bool> {
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;

View file

@ -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<Color>,
@ -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);

View file

@ -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<Vec2>,
}
// TODO: auto-shink/grow should be part of another container!
#[derive(Clone, Copy, Debug)]
pub struct Resize {
id: Option<Id>,
/// 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);

View file

@ -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();

View file

@ -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<FrameInteraction> {
{
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<FrameInteraction> {
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,

View file

@ -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<Self>, 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);

View file

@ -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())

View file

@ -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<Id, scroll_area::State>,
pub(crate) text_edit: HashMap<Id, text_edit::State>,
#[serde(skip)]
pub(crate) frame_interaction: Option<window::FrameInteraction>,
pub(crate) areas: Areas,
}

View file

@ -30,7 +30,10 @@ pub enum CursorIcon {
Default,
/// Pointing hand, used for e.g. web links
PointingHand,
ResizeHorizontal,
ResizeNeSw,
ResizeNwSe,
ResizeVertical,
Text,
}

View file

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