diff --git a/emigui/src/color.rs b/emigui/src/color.rs index 3f4e0eaa..3ec537ae 100644 --- a/emigui/src/color.rs +++ b/emigui/src/color.rs @@ -37,4 +37,5 @@ pub const WHITE: Color = srgba(255, 255, 255, 255); pub const RED: Color = srgba(255, 0, 0, 255); pub const GREEN: Color = srgba(0, 255, 0, 255); pub const BLUE: Color = srgba(0, 0, 255, 255); +pub const YELLOW: Color = srgba(255, 255, 0, 255); pub const LIGHT_BLUE: Color = srgba(140, 160, 255, 255); diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 07aec083..0b772bd5 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -128,7 +128,7 @@ impl Context { } } - pub fn interact(&self, layer: Layer, rect: Rect, interaction_id: Option) -> InteractInfo { + pub fn interact(&self, layer: Layer, rect: &Rect, interaction_id: Option) -> InteractInfo { let hovered = self.contains_mouse_pos(layer, &rect); let mut memory = self.memory.lock(); @@ -139,7 +139,7 @@ impl Context { if memory.active_id.is_some() { // Already clicked something else this frame InteractInfo { - rect, + rect: *rect, hovered, clicked: false, active: false, @@ -147,7 +147,7 @@ impl Context { } else { memory.active_id = interaction_id; InteractInfo { - rect, + rect: *rect, hovered, clicked: false, active: true, @@ -155,7 +155,7 @@ impl Context { } } else { InteractInfo { - rect, + rect: *rect, hovered, clicked: false, active: false, @@ -163,21 +163,21 @@ impl Context { } } else if self.input.mouse_released { InteractInfo { - rect, + rect: *rect, hovered, clicked: hovered && active, active, } } else if self.input.mouse_down { InteractInfo { - rect, + rect: *rect, hovered: hovered && active, clicked: false, active, } } else { InteractInfo { - rect, + rect: *rect, hovered, clicked: false, active, @@ -187,11 +187,11 @@ impl Context { pub fn show_error(&self, pos: Pos2, text: &str) { let align = (Align::Min, Align::Min); - let layer = Layer::Popup; // TODO: Layer::Error + let layer = Layer::Popup; // TODO: Layer::Debug let text_style = TextStyle::Monospace; let font = &self.fonts[text_style]; let (text, size) = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, size), align); + let rect = align_rect(&Rect::from_min_size(pos, size), align); self.add_paint_cmd( layer, PaintCmd::Rect { @@ -204,6 +204,19 @@ impl Context { self.add_text(layer, rect.min(), text_style, text, Some(color::RED)); } + pub fn debug_text(&self, pos: Pos2, text: &str) { + let layer = Layer::Popup; // TODO: Layer::Debug + let align = (Align::Min, Align::Min); + self.floating_text( + layer, + pos, + text, + TextStyle::Monospace, + align, + Some(color::YELLOW), + ); + } + /// Show some text anywhere on screen. /// To center the text at the given position, use `align: (Center, Center)`. pub fn floating_text( @@ -217,7 +230,7 @@ impl Context { ) -> Vec2 { let font = &self.fonts[text_style]; let (text, size) = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, size), align); + let rect = align_rect(&Rect::from_min_size(pos, size), align); self.add_text(layer, rect.min(), text_style, text, text_color); size } diff --git a/emigui/src/example_app.rs b/emigui/src/example_app.rs index 09e9ab16..1d75dded 100644 --- a/emigui/src/example_app.rs +++ b/emigui/src/example_app.rs @@ -1,7 +1,4 @@ -use crate::{ - color::*, label, math::*, widgets::*, Align, CollapsingHeader, Outline, PaintCmd, Region, - ScrollArea, -}; +use crate::{color::*, widgets::*, *}; /// Showcase some region code pub struct ExampleApp { @@ -150,9 +147,21 @@ impl ExampleApp { }); CollapsingHeader::new("Painting") - .default_open() + // .default_open() .show(region, |region| self.painting.ui(region)); + CollapsingHeader::new("Resize") + .default_open() + .show(region, |region| { + Resize::default() + .default_height(200.0) + // .as_wide_as_possible() + .show(region, |region| { + region.add(label!("This region can be resized!")); + region.add(label!("Just pull the handle on the bottom right")); + }); + }); + region.collapsing("Name clash example", |region| { region.add_label("\ Regions that store state require unique identifiers so we can track their state between frames. \ diff --git a/emigui/src/layout.rs b/emigui/src/layout.rs index ab035bbe..1d7e5237 100644 --- a/emigui/src/layout.rs +++ b/emigui/src/layout.rs @@ -76,7 +76,7 @@ impl Default for Align { } } -pub fn align_rect(rect: Rect, align: (Align, Align)) -> Rect { +pub fn align_rect(rect: &Rect, align: (Align, Align)) -> Rect { let x = match align.0 { Align::Min => rect.left(), Align::Center => rect.left() - 0.5 * rect.width(), diff --git a/emigui/src/lib.rs b/emigui/src/lib.rs index a38db2d4..90ebb520 100644 --- a/emigui/src/lib.rs +++ b/emigui/src/lib.rs @@ -20,6 +20,7 @@ pub mod math; mod memory; pub mod mesher; mod region; +mod resize; mod scroll_area; mod style; mod texture_atlas; @@ -40,6 +41,7 @@ pub use { memory::Memory, mesher::{Mesh, PaintBatches, Vertex}, region::Region, + resize::Resize, scroll_area::ScrollArea, style::Style, texture_atlas::Texture, diff --git a/emigui/src/math.rs b/emigui/src/math.rs index 55d9f7b5..c4fedf46 100644 --- a/emigui/src/math.rs +++ b/emigui/src/math.rs @@ -11,6 +11,17 @@ pub fn vec2(x: f32, y: f32) -> Vec2 { } impl Vec2 { + pub fn zero() -> Self { + Self { x: 0.0, y: 0.0 } + } + + pub fn infinity() -> Self { + Self { + x: f32::INFINITY, + y: f32::INFINITY, + } + } + pub fn splat(v: impl Into) -> Self { let v: f32 = v.into(); Self { x: v, y: v } @@ -50,14 +61,17 @@ impl Vec2 { vec2(angle.cos(), angle.sin()) } + #[must_use] pub fn floor(self) -> Self { vec2(self.x.floor(), self.y.floor()) } + #[must_use] pub fn round(self) -> Self { vec2(self.x.round(), self.y.round()) } + #[must_use] pub fn ceil(self) -> Self { vec2(self.x.ceil(), self.y.ceil()) } @@ -66,13 +80,23 @@ impl Vec2 { self.x.is_finite() && self.y.is_finite() } + #[must_use] pub fn min(self, other: Self) -> Self { vec2(self.x.min(other.x), self.y.min(other.y)) } + #[must_use] pub fn max(self, other: Self) -> Self { vec2(self.x.max(other.x), self.y.max(other.y)) } + + #[must_use] + pub fn clamp(self, range: RangeInclusive) -> Self { + Self { + x: clamp(self.x, range.start().x..=range.end().x), + y: clamp(self.y, range.start().y..=range.end().y), + } + } } impl PartialEq for Vec2 { @@ -210,13 +234,23 @@ impl Pos2 { self.x.is_finite() && self.y.is_finite() } + #[must_use] pub fn min(self, other: Self) -> Self { pos2(self.x.min(other.x), self.y.min(other.y)) } + #[must_use] pub fn max(self, other: Self) -> Self { pos2(self.x.max(other.x), self.y.max(other.y)) } + + #[must_use] + pub fn clamp(self, range: RangeInclusive) -> Self { + Self { + x: clamp(self.x, range.start().x..=range.end().x), + y: clamp(self.y, range.start().y..=range.end().y), + } + } } impl PartialEq for Pos2 { diff --git a/emigui/src/memory.rs b/emigui/src/memory.rs index ddaa2cd4..d4781988 100644 --- a/emigui/src/memory.rs +++ b/emigui/src/memory.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::{collapsing_header, scroll_area, window, *}; +use crate::{collapsing_header, resize, scroll_area, window, *}; #[derive(Clone, Debug, Default)] pub struct Memory { @@ -10,6 +10,7 @@ pub struct Memory { // states of various types of widgets pub(crate) collapsing_headers: HashMap, pub(crate) scroll_areas: HashMap, + pub(crate) resize: HashMap, windows: HashMap, /// Top is last diff --git a/emigui/src/region.rs b/emigui/src/region.rs index c07d2d84..eb5fc390 100644 --- a/emigui/src/region.rs +++ b/emigui/src/region.rs @@ -126,6 +126,10 @@ impl Region { self.ctx.input() } + pub fn memory(&self) -> parking_lot::MutexGuard { + self.ctx.memory.lock() + } + pub fn fonts(&self) -> &Fonts { &*self.ctx.fonts } @@ -308,9 +312,13 @@ impl Region { // ------------------------------------------------------------------------ /// Check for clicks on this entire region (desired_rect) - pub fn interact(&self) -> InteractInfo { + pub fn interact_whole(&self) -> InteractInfo { self.ctx - .interact(self.layer, self.desired_rect, Some(self.id)) + .interact(self.layer, &self.desired_rect, Some(self.id)) + } + + pub fn interact_rect(&self, rect: &Rect, id: Id) -> InteractInfo { + self.ctx.interact(self.layer, rect, Some(id)) } // ------------------------------------------------------------------------ @@ -342,7 +350,7 @@ impl Region { pub fn reserve_space(&mut self, size: Vec2, interaction_id: Option) -> InteractInfo { let pos = self.reserve_space_without_padding(size + self.style.item_spacing); let rect = Rect::from_min_size(pos, size); - self.ctx.interact(self.layer, rect, interaction_id) + self.ctx.interact(self.layer, &rect, interaction_id) } /// Reserve this much space and move the cursor. @@ -394,6 +402,13 @@ impl Region { self.id.with(id_seed) } + // ------------------------------------------------ + + /// Paint some debug text at current cursor + pub fn debug_text(&self, text: &str) { + self.ctx.debug_text(self.cursor, text); + } + /// Show some text anywhere in the region. /// To center the text at the given position, use `align: (Center, Center)`. /// If you want to draw text floating on top of everything, @@ -408,7 +423,7 @@ impl Region { ) -> Vec2 { let font = &self.fonts()[text_style]; let (text, size) = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, size), align); + let rect = align_rect(&Rect::from_min_size(pos, size), align); self.add_text(rect.min(), text_style, text, text_color); size } diff --git a/emigui/src/resize.rs b/emigui/src/resize.rs new file mode 100644 index 00000000..f99c966c --- /dev/null +++ b/emigui/src/resize.rs @@ -0,0 +1,146 @@ +#![allow(unused_variables)] // TODO +use crate::*; + +#[derive(Clone, Copy, Debug)] +pub struct State { + pub size: Vec2, +} + +#[derive(Clone, Copy, Debug)] +pub struct Resize { + // Will still try to stay within parent region bounds + min_size: Vec2, + max_size: Vec2, + + default_size: Vec2, + + // If true, won't allow you to make window so big that it creates spacing + shrink_width_to_fit_content: bool, + shrink_height_to_fit_content: bool, + + // If true, won't allow you to resize smaller than that everything fits. + expand_width_to_fit_content: bool, + expand_height_to_fit_content: bool, +} + +impl Default for Resize { + fn default() -> Self { + Self { + min_size: Vec2::splat(32.0), + max_size: Vec2::infinity(), + default_size: vec2(f32::INFINITY, 200.0), // TODO + shrink_width_to_fit_content: false, + shrink_height_to_fit_content: false, + expand_width_to_fit_content: true, + expand_height_to_fit_content: true, + } + } +} + +impl Resize { + pub fn default_height(mut self, height: f32) -> Self { + self.default_size.y = height; + self + } + + pub fn as_wide_as_possible(mut self) -> Self { + self.min_size.x = f32::INFINITY; + self + } +} + +// TODO: a common trait for Things that follow this pattern +impl Resize { + pub fn show(mut self, region: &mut Region, add_contents: impl FnOnce(&mut Region)) { + let id = region.make_child_id("scroll"); + self.min_size = self.min_size.min(region.available_space()); + self.max_size = self.max_size.min(region.available_space()); + self.max_size = self.max_size.max(self.min_size); + + let (is_new, mut state) = match region.memory().resize.get(&id) { + Some(state) => (false, state.clone()), + None => { + let default_size = self.default_size.clamp(self.min_size..=self.max_size); + (true, State { size: default_size }) + } + }; + + state.size = state.size.clamp(self.min_size..=self.max_size); + + let position = region.cursor(); + + // Resize-corner: + let corner_size = Vec2::splat(16.0); // TODO: style + let corner_rect = Rect::from_min_size(position + state.size - corner_size, corner_size); + let corner_interact = region.interact_rect(&corner_rect, id.with("corner")); + + if corner_interact.active { + if let Some(mouse_pos) = region.input().mouse_pos { + state.size = mouse_pos - position + 0.5 * corner_interact.rect.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(region.cursor(), state.size); + let desired_size = { + let mut contents_region = region.child_region(inner_rect); + add_contents(&mut contents_region); + let desired_size = contents_region.bounding_size; + desired_size + }; + let desired_size = desired_size.ceil(); // Avoid rounding errors in math + + // ------------------------------ + + if self.shrink_width_to_fit_content { + state.size.x = state.size.x.min(desired_size.x); + } + if self.shrink_height_to_fit_content { + state.size.y = state.size.y.min(desired_size.y); + } + if self.expand_width_to_fit_content || is_new { + state.size.x = state.size.x.max(desired_size.x); + } + if self.expand_height_to_fit_content || is_new { + state.size.y = state.size.y.max(desired_size.y); + } + + state.size = state.size.max(self.min_size); + // state.size = state.size.clamp(self.min_size..=self.max_size); + state.size = state.size.round(); // TODO: round to pixels + + region.reserve_space_without_padding(state.size); + + // ------------------------------ + + paint_resize_corner(region, &corner_rect, &corner_interact); + + if corner_interact.hovered || corner_interact.active { + region.ctx().output.lock().cursor_icon = CursorIcon::ResizeNwSe; + } + + region.memory().resize.insert(id, state); + } +} + +fn paint_resize_corner(region: &mut Region, rect: &Rect, interact: &InteractInfo) { + let color = region.style().interact_stroke_color(&interact); + let width = region.style().interact_stroke_width(&interact); + + let corner = rect.right_bottom().round(); // TODO: round to pixels + let mut w = 2.0; + + while w < 12.0 { + region.add_paint_cmd(PaintCmd::line_segment( + (pos2(corner.x - w, corner.y), pos2(corner.x, corner.y - w)), + color, + width, + )); + w += 4.0; + } +} diff --git a/emigui/src/scroll_area.rs b/emigui/src/scroll_area.rs index cc46d5f9..77730905 100644 --- a/emigui/src/scroll_area.rs +++ b/emigui/src/scroll_area.rs @@ -57,7 +57,7 @@ impl ScrollArea { let content_interact = ctx.interact( outer_region.layer, - inner_rect, + &inner_rect, Some(scroll_area_id.with("area")), ); if content_interact.active { @@ -95,7 +95,7 @@ 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 handle_interact = ctx.interact(outer_region.layer, &handle_rect, interact_id); if let Some(mouse_pos) = ctx.input.mouse_pos { if handle_interact.active { @@ -106,7 +106,7 @@ impl ScrollArea { } else { // Check for mouse down outside handle: let scroll_bg_interact = - ctx.interact(outer_region.layer, outer_scroll_rect, interact_id); + ctx.interact(outer_region.layer, &outer_scroll_rect, interact_id); if scroll_bg_interact.active { // Center scroll at mouse pos: diff --git a/emigui/src/types.rs b/emigui/src/types.rs index ccd44d24..6a723aa0 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -180,3 +180,13 @@ pub enum PaintCmd { /// Low-level triangle mesh Mesh(Mesh), } + +impl PaintCmd { + pub fn line_segment(seg: (Pos2, Pos2), color: Color, width: f32) -> Self { + Self::Line { + points: vec![seg.0, seg.1], + color, + width, + } + } +} diff --git a/emigui/src/window.rs b/emigui/src/window.rs index 1ba17362..0cb838ca 100644 --- a/emigui/src/window.rs +++ b/emigui/src/window.rs @@ -45,7 +45,7 @@ impl Default for Window { default_size: None, resizeable: true, shrink_width_to_fit_content: false, - shrink_height_to_fit_content: true, + shrink_height_to_fit_content: false, expand_width_to_fit_content: true, expand_height_to_fit_content: true, min_size: Vec2::splat(16.0), @@ -121,7 +121,7 @@ impl Window { let id = ctx.make_unique_id(&self.title, default_pos); - let (mut state, is_new_window) = match ctx.memory.lock().get_window(id) { + let (mut state, is_new) = match ctx.memory.lock().get_window(id) { Some(state) => (state, false), None => { let state = State { @@ -167,10 +167,10 @@ impl Window { if self.shrink_height_to_fit_content { new_inner_size.y = new_inner_size.y.min(desired_inner_size.y); } - if self.expand_width_to_fit_content || is_new_window { + if self.expand_width_to_fit_content || is_new { new_inner_size.x = new_inner_size.x.max(desired_inner_size.x); } - if self.expand_height_to_fit_content || is_new_window { + if self.expand_height_to_fit_content || is_new { new_inner_size.y = new_inner_size.y.max(desired_inner_size.y); } new_inner_size = new_inner_size.max(min_inner_size); @@ -201,7 +201,7 @@ impl Window { let corner_center = outer_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"))); + let corner_interact = ctx.interact(layer, &corner_rect, Some(id.with("corner"))); graphics.layer(layer).push(( Rect::everything(), @@ -212,7 +212,7 @@ impl Window { InteractInfo::default() }; - let win_interact = ctx.interact(layer, outer_rect, Some(id.with("window"))); + let win_interact = ctx.interact(layer, &outer_rect, Some(id.with("window"))); if corner_interact.active { if let Some(mouse_pos) = ctx.input().mouse_pos { diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index 50dfe5f3..dc207bfa 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -115,7 +115,7 @@ fn main() { Window::new("Examples") .default_pos(pos2(50.0, 100.0)) - .default_size(vec2(300.0, 400.0)) + .default_size(vec2(300.0, 600.0)) .show(region.ctx(), |region| { example_app.ui(region); });