From 388132ba93bc34c57c1c375e8a696a207d2f07ba Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 19 Apr 2020 23:34:34 +0200 Subject: [PATCH] Implement window resizing --- emigui/src/math.rs | 20 +++++++++ emigui/src/memory.rs | 10 ++++- emigui/src/mesher.rs | 40 ++++++++++++++++- emigui/src/region.rs | 2 +- emigui/src/style.rs | 41 +++++++++++++++-- emigui/src/types.rs | 11 +++-- emigui/src/widgets.rs | 8 ++-- emigui/src/window.rs | 101 +++++++++++++++++++++++++++++++++++------- 8 files changed, 202 insertions(+), 31 deletions(-) diff --git a/emigui/src/math.rs b/emigui/src/math.rs index eaf8e463..bf513931 100644 --- a/emigui/src/math.rs +++ b/emigui/src/math.rs @@ -43,6 +43,26 @@ impl Vec2 { pub fn angled(angle: f32) -> Vec2 { vec2(angle.cos(), angle.sin()) } + + pub fn floor(self) -> Self { + vec2(self.x.floor(), self.y.floor()) + } + + pub fn round(self) -> Self { + vec2(self.x.round(), self.y.round()) + } + + pub fn ceil(self) -> Self { + vec2(self.x.ceil(), self.y.ceil()) + } + + pub fn min(self, other: Vec2) -> Self { + vec2(self.x.min(other.x), self.y.min(other.y)) + } + + pub fn max(self, other: Vec2) -> Self { + vec2(self.x.max(other.x), self.y.max(other.y)) + } } impl std::ops::Neg for Vec2 { diff --git a/emigui/src/memory.rs b/emigui/src/memory.rs index bdeb9e4c..1bdca9ba 100644 --- a/emigui/src/memory.rs +++ b/emigui/src/memory.rs @@ -29,8 +29,16 @@ impl Memory { } } + /// default_rect: where to put it if it does NOT exist + pub fn get_window(&mut self, id: Id) -> Option { + self.windows.get(&id).cloned() + } + pub fn set_window_state(&mut self, id: Id, state: WindowState) { - self.windows.insert(id, state); + let did_insert = self.windows.insert(id, state).is_none(); + if did_insert { + self.window_order.push(id); + } } pub fn layer_at(&self, pos: Pos2) -> Layer { diff --git a/emigui/src/mesher.rs b/emigui/src/mesher.rs index d53cf36b..a02e00e1 100644 --- a/emigui/src/mesher.rs +++ b/emigui/src/mesher.rs @@ -118,6 +118,7 @@ impl Mesh { // ---------------------------------------------------------------------------- +#[derive(Clone, Debug, Default)] pub struct PathPoint { pos: Pos2, @@ -129,8 +130,8 @@ pub struct PathPoint { normal: Vec2, } -#[derive(Default)] -struct Path(Vec); +#[derive(Clone, Debug, Default)] +pub struct Path(Vec); impl Path { pub fn clear(&mut self) { @@ -195,6 +196,17 @@ impl Path { } } + /// with x right, and y down (GUI coords) we have: + /// angle = dir + /// 0 * TAU / 4 = right + /// quadrant 0, right down + /// 1 * TAU / 4 = down + /// quadrant 1, down left + /// 2 * TAU / 4 = left + /// quadrant 2 left up + /// 3 * TAU / 4 = up + /// quadrant 3 up rigth + /// 4 * TAU / 4 = right pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) { let n = 8; const RIGHT_ANGLE: f32 = TAU / 4.0; @@ -421,6 +433,30 @@ impl Mesher { paint_path(&mut self.mesh, &self.options, Open, &path.0, *color, *width); } } + PaintCmd::Path { + path, + closed, + fill_color, + outline, + } => { + if let Some(fill_color) = fill_color { + debug_assert!( + *closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + fill_closed_path(&mut self.mesh, &self.options, &path.0, *fill_color); + } + if let Some(outline) = outline { + paint_path( + &mut self.mesh, + &self.options, + Closed, + &path.0, + outline.color, + outline.width, + ); + } + } PaintCmd::Rect { corner_radius, fill_color, diff --git a/emigui/src/region.rs b/emigui/src/region.rs index ab24fb73..74d029e0 100644 --- a/emigui/src/region.rs +++ b/emigui/src/region.rs @@ -130,7 +130,7 @@ impl Region { self.add_paint_cmd(PaintCmd::Rect { corner_radius: 5.0, - fill_color: Some(fill_color), + fill_color, outline: Some(Outline::new(1.0, color::WHITE)), rect: interact.rect, }); diff --git a/emigui/src/style.rs b/emigui/src/style.rs index d99949a2..933590ed 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -25,6 +25,13 @@ pub struct Style { // Purely visual: /// For stuff like check marks in check boxes. pub line_width: f32, + + pub window: Window, +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct Window { + pub corner_radius: f32, } impl Default for Style { @@ -37,6 +44,15 @@ impl Default for Style { clickable_diameter: 34.0, start_icon_width: 20.0, line_width: 2.0, + window: Window::default(), + } + } +} + +impl Default for Window { + fn default() -> Self { + Window { + corner_radius: 10.0, } } } @@ -52,13 +68,13 @@ impl Style { } /// Fill color of the interactive part of a component (button, slider grab, checkbox, ...) - pub fn interact_fill_color(&self, interact: &InteractInfo) -> Color { + pub fn interact_fill_color(&self, interact: &InteractInfo) -> Option { if interact.active { - srgba(100, 100, 200, 255) + Some(srgba(100, 100, 200, 255)) } else if interact.hovered { - srgba(100, 100, 150, 255) + Some(srgba(100, 100, 150, 255)) } else { - srgba(60, 60, 70, 255) + Some(srgba(60, 60, 70, 255)) } } @@ -73,6 +89,23 @@ impl Style { } } + pub fn interact_stroke_width(&self, interact: &InteractInfo) -> f32 { + if interact.active { + 2.0 + } else if interact.hovered { + 1.5 + } else { + 1.0 + } + } + + pub fn interact_outline(&self, interact: &InteractInfo) -> Option { + Some(Outline::new( + self.interact_stroke_width(interact), + self.interact_stroke_color(interact), + )) + } + /// Returns small icon rectangle and big icon rectangle pub fn icon_rectangles(&self, rect: &Rect) -> (Rect, Rect) { let box_side = 16.0; diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 814b8976..f1f16562 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -2,7 +2,7 @@ use crate::{ color::Color, fonts::TextStyle, math::{Pos2, Rect, Vec2}, - mesher::Mesh, + mesher::{Mesh, Path}, }; // ---------------------------------------------------------------------------- @@ -100,8 +100,7 @@ impl Outline { } } -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "snake_case", tag = "kind")] +#[derive(Clone, Debug)] pub enum PaintCmd { Circle { center: Pos2, @@ -114,6 +113,12 @@ pub enum PaintCmd { color: Color, width: f32, }, + Path { + path: Path, + closed: bool, + fill_color: Option, + outline: Option, + }, Rect { corner_radius: f32, fill_color: Option, diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 0920f99e..17142e62 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -91,7 +91,7 @@ impl Widget for Button { let text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * text_size.y); region.add_paint_cmd(PaintCmd::Rect { corner_radius: 10.0, - fill_color: Some(region.style().interact_fill_color(&interact)), + fill_color: region.style().interact_fill_color(&interact), outline: None, rect: interact.rect, }); @@ -146,7 +146,7 @@ impl<'a> Widget for Checkbox<'a> { let (small_icon_rect, big_icon_rect) = region.style().icon_rectangles(&interact.rect); region.add_paint_cmd(PaintCmd::Rect { corner_radius: 3.0, - fill_color: Some(region.style().interact_fill_color(&interact)), + fill_color: region.style().interact_fill_color(&interact), outline: None, rect: big_icon_rect, }); @@ -222,7 +222,7 @@ impl Widget for RadioButton { region.add_paint_cmd(PaintCmd::Circle { center: big_icon_rect.center(), - fill_color: Some(fill_color), + fill_color, outline: None, radius: big_icon_rect.size().x / 2.0, }); @@ -415,7 +415,7 @@ impl<'a> Widget for Slider<'a> { region.add_paint_cmd(PaintCmd::Circle { center: pos2(marker_center_x, thin_rect.center().y), - fill_color: Some(region.style().interact_fill_color(&interact)), + fill_color: region.style().interact_fill_color(&interact), outline: Some(Outline::new( 1.5, region.style().interact_stroke_color(&interact), diff --git a/emigui/src/window.rs b/emigui/src/window.rs index e58da044..738dbfd2 100644 --- a/emigui/src/window.rs +++ b/emigui/src/window.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{layout::Direction, widgets::Label, *}; +use crate::{layout::Direction, mesher::Path, widgets::Label, *}; #[derive(Clone, Copy, Debug)] pub struct WindowState { @@ -8,12 +8,32 @@ pub struct WindowState { pub rect: Rect, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct Window { /// The title of the window and by default the source of its identity. title: String, /// Put the window here the first time default_pos: Option, + + resizeable: bool, + + // If true, won't allow you to make window so big that it creates spacing + shrink_to_fit_content: bool, + + // If true, won't allow you to resize smaller than that everything fits. + expand_to_fit_content: bool, +} + +impl Default for Window { + fn default() -> Self { + Self { + title: "".to_owned(), + default_pos: None, + resizeable: true, + shrink_to_fit_content: false, // Normally you want this when resizable = false + expand_to_fit_content: true, + } + } } impl Window { @@ -29,21 +49,24 @@ impl Window { self } + pub fn resizeable(mut self, resizeable: bool) -> Self { + self.resizeable = resizeable; + self + } + pub fn show(self, ctx: &Arc, add_contents: F) where F: FnOnce(&mut Region), { let default_pos = self.default_pos.unwrap_or(pos2(100.0, 100.0)); // TODO + let default_size = vec2(200.0, 50.0); // TODO let id = ctx.make_unique_id(&self.title, default_pos); - let mut state = ctx.memory.lock().get_or_create_window( - id, - Rect::from_min_size( - default_pos, - vec2(200.0, 200.0), // TODO - ), - ); + let mut state = ctx + .memory + .lock() + .get_or_create_window(id, Rect::from_min_size(default_pos, default_size)); let layer = Layer::Window(id); let where_to_put_background = ctx.graphics.lock().layer(layer).len(); @@ -60,7 +83,7 @@ impl Window { align: Align::Min, cursor: state.rect.min() + window_padding, bounding_size: vec2(0.0, 0.0), - available_space: vec2(ctx.input.screen_size.x.min(350.0), std::f32::INFINITY), // TODO: window.width + available_space: state.rect.size() - 2.0 * window_padding, }; // Show top bar: @@ -72,28 +95,74 @@ impl Window { // TODO: handle the last item_spacing in a nicer way let inner_size = contents_region.bounding_size - style.item_spacing; - let outer_size = inner_size + 2.0 * window_padding; + let inner_size = inner_size.ceil(); // Avoid rounding errors in math + let desired_outer_size = inner_size + 2.0 * window_padding; + let mut new_outer_size = state.rect.size(); - state.rect = Rect::from_min_size(state.rect.min(), outer_size); + if self.shrink_to_fit_content { + new_outer_size = new_outer_size.min(desired_outer_size); + } + + if self.expand_to_fit_content { + new_outer_size = new_outer_size.max(desired_outer_size); + } + + state.rect = Rect::from_min_size(state.rect.min(), new_outer_size); let mut graphics = ctx.graphics.lock(); + + let corner_radius = style.window.corner_radius; graphics.layer(layer).insert( where_to_put_background, PaintCmd::Rect { - corner_radius: 5.0, + corner_radius, fill_color: Some(style.background_fill_color()), outline: Some(Outline::new(1.0, color::WHITE)), rect: state.rect, }, ); - let interact = ctx.interact(layer, state.rect, Some(id)); - if interact.active { + let corner_interact = if self.resizeable { + // Resize-corner: + let mut path = Path::default(); + let quadrant = 0.0; // Bottom-right + let corner_center = state.rect.max() - Vec2::splat(corner_radius); + let corner_rect = Rect::from_min_size(corner_center, Vec2::splat(corner_radius)); + + let corner_interact = ctx.interact(layer, corner_rect, Some(id.with(&"corner"))); + + // TODO: Path::circle_sector() or something + path.add_point(corner_center, vec2(0.0, -1.0)); + path.add_point(corner_center + vec2(corner_radius, 0.0), vec2(0.0, -1.0)); + path.add_circle_quadrant(corner_center, corner_radius, quadrant); + path.add_point(corner_center + vec2(0.0, corner_radius), vec2(-1.0, 0.0)); + path.add_point(corner_center, vec2(-1.0, 0.0)); + graphics.layer(layer).insert( + where_to_put_background + 1, + PaintCmd::Path { + path, + closed: true, + fill_color: style.interact_fill_color(&corner_interact), + outline: style.interact_outline(&corner_interact), + }, + ); + corner_interact + } else { + InteractInfo::default() + }; + + let win_interact = ctx.interact(layer, state.rect, Some(id.with(&"window"))); + + if corner_interact.active { + let new_size = state.rect.size() + ctx.input().mouse_move; + let new_size = new_size.max(Vec2::splat(0.0)); + state.rect = Rect::from_min_size(state.rect.min(), new_size); + } else if win_interact.active { state.rect = state.rect.translate(ctx.input().mouse_move); } let mut memory = ctx.memory.lock(); - if interact.active || interact.clicked { + if win_interact.active || corner_interact.active { memory.move_window_to_top(id); } memory.set_window_state(id, state);