diff --git a/emigui/README.md b/emigui/README.md index b730210a..84527677 100644 --- a/emigui/README.md +++ b/emigui/README.md @@ -8,7 +8,7 @@ This is the core library crate Emigui. It is fully platform independent without * [x] Checkbox * [x] Radiobutton * [x] Slider -* [x] Foldable region +* [x] Collapsing header region * [x] Tooltip * [x] Movable/resizable windows * [ ] Kinetic windows @@ -26,7 +26,7 @@ This is the core library crate Emigui. It is fully platform independent without ### Animations Add extremely quick animations for some things, maybe 2-3 frames. For instance: -* [x] Animate foldables with clip_rect +* [x] Animate collapsing headers with clip_rect ### Clip rects * [x] Separate Region::clip_rect from Region::rect @@ -52,7 +52,6 @@ I think A) is the correct solution, but might be tedious to get right for every ### Names and structure * [ ] Rename things to be more consistent with Dear ImGui - * Foldable -> Collapsible etc * [ ] Combine Emigui and Context * [ ] Rename Region to something shorter? * `region: &Region` `region.add(...)` :/ @@ -60,4 +59,4 @@ I think A) is the correct solution, but might be tedious to get right for every * `ui: &Ui` `ui.add(...)` :) ### Global widget search -Ability to do a search for any widget. The search works even for closed windows and foldables. This is implemented like this: while searching, all region are layed out and their add_content functions are run. If none of the contents matches the search, the layout is reverted and nothing is shown. So windows will get temporarily opened and run, but if the search is not a match in the window it is closed again. This means then when searching your whole GUI is being run, which may be a bit slower, but it would be a really awesome feature. +Ability to do a search for any widget. The search works even for collapsed regions and closed windows and menus. This is implemented like this: while searching, all region are layed out and their add_content functions are run. If none of the contents matches the search, the layout is reverted and nothing is shown. So windows will get temporarily opened and run, but if the search is not a match in the window it is closed again. This means then when searching your whole GUI is being run, which may be a bit slower, but it would be a really awesome feature. diff --git a/emigui/src/collapsing_header.rs b/emigui/src/collapsing_header.rs new file mode 100644 index 00000000..e3f5077d --- /dev/null +++ b/emigui/src/collapsing_header.rs @@ -0,0 +1,156 @@ +use crate::{layout::Direction, *}; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct State { + pub open: bool, + pub toggle_time: f64, +} + +impl Default for State { + fn default() -> Self { + Self { + open: false, + toggle_time: -std::f64::INFINITY, + } + } +} + +pub struct CollapsingHeader { + title: String, + default_open: bool, +} + +impl CollapsingHeader { + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + default_open: false, + } + } + + pub fn default_open(mut self) -> Self { + self.default_open = true; + self + } +} + +impl CollapsingHeader { + pub fn show(self, region: &mut Region, add_contents: impl FnOnce(&mut Region)) -> GuiResponse { + assert!( + region.dir == Direction::Vertical, + "Horizontal collapsing is unimplemented" + ); + let Self { + title, + default_open, + } = self; + + let id = region.make_unique_id(&title); + let text_style = TextStyle::Button; + let font = ®ion.fonts()[text_style]; + let (title, text_size) = font.layout_multiline(&title, region.available_width()); + let text_cursor = region.cursor + region.style.button_padding; + let interact = region.reserve_space( + vec2( + region.available_width(), + text_size.y + 2.0 * region.style.button_padding.y, + ), + Some(id), + ); + + let state = { + let mut memory = region.ctx.memory.lock(); + let mut state = memory.collapsing_headers.entry(id).or_insert(State { + open: default_open, + ..Default::default() + }); + if interact.clicked { + state.open = !state.open; + state.toggle_time = region.ctx.input.time; + } + *state + }; + + region.add_paint_cmd(PaintCmd::Rect { + corner_radius: region.style.interact_corner_radius(&interact), + fill_color: region.style.interact_fill_color(&interact), + outline: region.style().interact_outline(&interact), + rect: interact.rect, + }); + + paint_icon(region, &state, &interact); + + region.add_text( + text_cursor + vec2(region.style.start_icon_width, 0.0), + text_style, + title, + Some(region.style.interact_stroke_color(&interact)), + ); + + let animation_time = region.style().animation_time; + let time_since_toggle = (region.ctx.input.time - state.toggle_time) as f32; + if time_since_toggle < animation_time { + region.indent(id, |region| { + // animation time + + let max_height = if state.open { + remap( + time_since_toggle, + 0.0, + animation_time, + 50.0, // Get instant feedback + 1500.0, // We don't expect to get bigger than this + ) + } else { + remap_clamp( + time_since_toggle, + 0.0, + animation_time, + 50.0, // TODO: state.open_height + 0.0, + ) + }; + + region + .clip_rect + .set_height(region.clip_rect.height().min(max_height)); + + add_contents(region); + + region.bounding_size.y = region.bounding_size.y.min(max_height); + }); + } else if state.open { + region.indent(id, add_contents); + } + + region.response(interact) + } +} + +fn paint_icon(region: &mut Region, state: &State, interact: &InteractInfo) { + let stroke_color = region.style.interact_stroke_color(&interact); + let stroke_width = region.style.interact_stroke_width(&interact); + + let (small_icon_rect, _) = region.style.icon_rectangles(&interact.rect); + // Draw a minus: + region.add_paint_cmd(PaintCmd::Line { + points: vec![ + pos2(small_icon_rect.min().x, small_icon_rect.center().y), + pos2(small_icon_rect.max().x, small_icon_rect.center().y), + ], + color: stroke_color, + width: stroke_width, + }); + + if !state.open { + // Draw it as a plus: + region.add_paint_cmd(PaintCmd::Line { + points: vec![ + pos2(small_icon_rect.center().x, small_icon_rect.min().y), + pos2(small_icon_rect.center().x, small_icon_rect.max().y), + ], + color: stroke_color, + width: stroke_width, + }); + } +} diff --git a/emigui/src/emigui.rs b/emigui/src/emigui.rs index fad1b744..e7c71292 100644 --- a/emigui/src/emigui.rs +++ b/emigui/src/emigui.rs @@ -77,7 +77,7 @@ impl Emigui { } pub fn ui(&mut self, region: &mut Region) { - region.foldable("Style", |region| { + region.collapsing("Style", |region| { region.add(Checkbox::new( &mut self.mesher_options.anti_alias, "Antialias", @@ -89,7 +89,7 @@ impl Emigui { self.ctx.style_ui(region); }); - region.foldable("Fonts", |region| { + region.collapsing("Fonts", |region| { let old_font_definitions = self.ctx.fonts.definitions(); let mut new_font_definitions = old_font_definitions.clone(); font_definitions_ui(&mut new_font_definitions, region); @@ -103,7 +103,7 @@ impl Emigui { } }); - region.foldable("Stats", |region| { + region.collapsing("Stats", |region| { region.add(label!( "Screen size: {} x {} points, pixels_per_point: {}", region.input().screen_size.x, diff --git a/emigui/src/example_app.rs b/emigui/src/example_app.rs index 8542af01..dbe8300d 100644 --- a/emigui/src/example_app.rs +++ b/emigui/src/example_app.rs @@ -1,4 +1,7 @@ -use crate::{color::*, label, math::*, widgets::*, Align, Outline, PaintCmd, Region, ScrollArea}; +use crate::{ + color::*, label, math::*, widgets::*, Align, CollapsingHeader, Outline, PaintCmd, Region, + ScrollArea, +}; /// Showcase some region code pub struct ExampleApp { @@ -36,13 +39,13 @@ impl Default for ExampleApp { impl ExampleApp { pub fn ui(&mut self, region: &mut Region) { - region.foldable("About Emigui", |region| { + region.collapsing("About Emigui", |region| { region.add(label!( "Emigui is an experimental immediate mode GUI written in Rust." )); }); - region.foldable("Widgets", |region| { + region.collapsing("Widgets", |region| { region.horizontal(Align::Min, |region| { region.add(label!("Text can have").text_color(srgba(110, 255, 110, 255))); region.add(label!("color").text_color(srgba(128, 140, 255, 255))); @@ -87,7 +90,7 @@ impl ExampleApp { region.add(label!("Value: {}", value)); }); - region.foldable("Layouts", |region| { + region.collapsing("Layouts", |region| { region.add(Slider::usize(&mut self.num_columns, 1, 10).text("Columns")); region.columns(self.num_columns, |cols| { for (i, col) in cols.iter_mut().enumerate() { @@ -101,7 +104,7 @@ impl ExampleApp { }); }); - region.foldable("Test box rendering", |region| { + region.collapsing("Test box rendering", |region| { region.add(Slider::f32(&mut self.size.x, 0.0, 500.0).text("width")); region.add(Slider::f32(&mut self.size.y, 0.0, 500.0).text("height")); region.add(Slider::f32(&mut self.corner_radius, 0.0, 50.0).text("corner_radius")); @@ -131,26 +134,28 @@ impl ExampleApp { region.add_paint_cmds(cmds); }); - region.foldable("Scroll area", |region| { - ScrollArea::default().show(region, |region| { - region.add_label(LOREM_IPSUM); + CollapsingHeader::new("Scroll area") + .default_open() + .show(region, |region| { + ScrollArea::default().show(region, |region| { + region.add_label(LOREM_IPSUM); + }); }); - }); - region.foldable("Name clash example", |region| { + region.collapsing("Name clash example", |region| { region.add_label("\ Regions that store state require unique identifiers so we can track their state between frames. \ Identifiers are normally derived from the titles of the widget."); region.add_label("\ - For instance, foldable regions needs to store wether or not they are open. \ + For instance, collapsing regions needs to store wether or not they are open. \ If you fail to give them unique names then clicking one will open both. \ To help you debug this, an error message is printed on screen:"); - region.foldable("Foldable", |region| { + region.collapsing("Collapsing header", |region| { region.add_label("Contents of first folddable region"); }); - region.foldable("Foldable", |region| { + region.collapsing("Collapsing header", |region| { region.add_label("Contents of second folddable region"); }); diff --git a/emigui/src/id.rs b/emigui/src/id.rs index 334092b6..24df133e 100644 --- a/emigui/src/id.rs +++ b/emigui/src/id.rs @@ -6,7 +6,7 @@ //! moved outside the slider. //! //! For some widgets `Id`s are also used to GUIpersist some state about the -//! widgets, such as Window position or wether not a Foldable region is open. +//! widgets, such as Window position or wether not a collapsing header region is open. //! //! This implicated that the `Id`s must be unqiue. //! @@ -15,10 +15,10 @@ //! For instance, a slider only needs a unique and persistent ID while you are //! dragging the sldier. As long as it is still while moving, that is fine. //! -//! For things that need to persist state even after moving (windows, foldables) +//! For things that need to persist state even after moving (windows, collapsing headers) //! the location of the widgets is obviously not good enough. For instance, //! a fodlable region needs to remember wether or not it is open even -//! if the layout next frame is different and the foldable is not lower down +//! if the layout next frame is different and the collapsing is not lower down //! on the screen. //! //! Then there are widgets that need no identifiers at all, like labels, diff --git a/emigui/src/lib.rs b/emigui/src/lib.rs index 7391e2d1..1b605a84 100644 --- a/emigui/src/lib.rs +++ b/emigui/src/lib.rs @@ -6,6 +6,7 @@ extern crate serde; #[macro_use] // TODO: get rid of this extern crate serde_derive; +mod collapsing_header; pub mod color; mod context; mod emigui; @@ -28,12 +29,13 @@ mod window; pub use { crate::emigui::Emigui, + collapsing_header::CollapsingHeader, color::Color, context::{Context, CursorIcon}, fonts::{FontDefinitions, Fonts, TextStyle}, id::Id, layers::*, - layout::Align, + layout::{Align, GuiResponse}, math::*, memory::Memory, mesher::{Mesh, PaintBatches, Vertex}, diff --git a/emigui/src/memory.rs b/emigui/src/memory.rs index 3372a693..e85b95cd 100644 --- a/emigui/src/memory.rs +++ b/emigui/src/memory.rs @@ -1,22 +1,7 @@ use std::collections::HashMap; -use crate::{window::WindowState, *}; +use crate::{collapsing_header, window::WindowState, *}; -// TODO: move together with foldable code into own file -#[derive(Clone, Copy, Debug)] -pub struct FoldableState { - pub open: bool, - pub toggle_time: f64, -} - -impl Default for FoldableState { - fn default() -> Self { - Self { - open: false, - toggle_time: -std::f64::INFINITY, - } - } -} #[derive(Clone, Copy, Debug, Default)] pub struct ScrollState { /// Positive offset means scrolling down/right @@ -29,7 +14,7 @@ pub struct Memory { pub(crate) active_id: Option, // states of various types of widgets - pub(crate) foldables: HashMap, + pub(crate) collapsing_headers: HashMap, pub(crate) scroll_areas: HashMap, windows: HashMap, diff --git a/emigui/src/region.rs b/emigui/src/region.rs index 6574467e..62773ac5 100644 --- a/emigui/src/region.rs +++ b/emigui/src/region.rs @@ -143,118 +143,6 @@ impl Region { // ------------------------------------------------------------------------ // Sub-regions: - pub fn foldable(&mut self, text: S, add_contents: F) -> GuiResponse - where - S: Into, - F: FnOnce(&mut Region), - { - assert!( - self.dir == Direction::Vertical, - "Horizontal foldable is unimplemented" - ); - let text: String = text.into(); - let id = self.make_unique_id(&text); - let text_style = TextStyle::Button; - let font = &self.fonts()[text_style]; - let (text, text_size) = font.layout_multiline(&text, self.available_width()); - let text_cursor = self.cursor + self.style.button_padding; - let interact = self.reserve_space( - vec2( - self.available_width(), - text_size.y + 2.0 * self.style.button_padding.y, - ), - Some(id), - ); - - let state = { - let mut memory = self.ctx.memory.lock(); - let mut state = memory.foldables.entry(id).or_default(); - if interact.clicked { - state.open = !state.open; - state.toggle_time = self.ctx.input.time; - } - *state - }; - - let fill_color = self.style.interact_fill_color(&interact); - let stroke_color = self.style.interact_stroke_color(&interact); - - self.add_paint_cmd(PaintCmd::Rect { - corner_radius: self.style.interaction_corner_radius, - fill_color, - outline: Some(Outline::new(1.0, color::WHITE)), - rect: interact.rect, - }); - - let (small_icon_rect, _) = self.style.icon_rectangles(&interact.rect); - // Draw a minus: - self.add_paint_cmd(PaintCmd::Line { - points: vec![ - pos2(small_icon_rect.min().x, small_icon_rect.center().y), - pos2(small_icon_rect.max().x, small_icon_rect.center().y), - ], - color: stroke_color, - width: self.style.line_width, - }); - - if !state.open { - // Draw it as a plus: - self.add_paint_cmd(PaintCmd::Line { - points: vec![ - pos2(small_icon_rect.center().x, small_icon_rect.min().y), - pos2(small_icon_rect.center().x, small_icon_rect.max().y), - ], - color: stroke_color, - width: self.style.line_width, - }); - } - - self.add_text( - text_cursor + vec2(self.style.start_icon_width, 0.0), - text_style, - text, - None, - ); - - let animation_time = self.style().animation_time; - let time_since_toggle = (self.ctx.input.time - state.toggle_time) as f32; - if time_since_toggle < animation_time { - self.indent(id, |region| { - // animation time - - let max_height = if state.open { - remap( - time_since_toggle, - 0.0, - animation_time, - 50.0, // Get instant feedback - 1500.0, // We don't expect to get bigger than this - ) - } else { - remap_clamp( - time_since_toggle, - 0.0, - animation_time, - 50.0, // TODO: state.open_height - 0.0, - ) - }; - - region - .clip_rect - .set_height(region.clip_rect.height().min(max_height)); - - add_contents(region); - - region.bounding_size.y = region.bounding_size.y.min(max_height); - }); - } else if state.open { - self.indent(id, add_contents); - } - - self.response(interact) - } - /// Create a child region which is indented to the right pub fn indent(&mut self, id: Id, add_contents: F) where @@ -453,6 +341,14 @@ impl Region { self.add(Label::new(text)) } + pub fn collapsing(&mut self, text: S, add_contents: F) -> GuiResponse + where + S: Into, + F: FnOnce(&mut Region), + { + CollapsingHeader::new(text).show(self, add_contents) + } + // ------------------------------------------------------------------------ pub fn reserve_space(&mut self, size: Vec2, interaction_id: Option) -> InteractInfo { diff --git a/emigui/src/style.rs b/emigui/src/style.rs index 7322329e..6d2deaf9 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -11,13 +11,13 @@ pub struct Style { /// Horizontal and vertical spacing between widgets pub item_spacing: Vec2, - /// Indent foldable regions etc by this much. + /// Indent collapsing regions etc by this much. pub indent: f32, /// Anything clickable is (at least) this wide. pub clickable_diameter: f32, - /// Checkboxes, radio button and foldables have an icon at the start. + /// Checkboxes, radio button and collapsing headers have an icon at the start. /// The text starts after this many pixels. pub start_icon_width: f32, @@ -108,11 +108,29 @@ impl Style { } } + /// For rectangles pub fn interact_outline(&self, interact: &InteractInfo) -> Option { + if interact.active { Some(Outline::new( self.interact_stroke_width(interact), self.interact_stroke_color(interact), )) + } else if interact.hovered { + None + } else { + None + } + } + + /// Buttons etc + pub fn interact_corner_radius(&self, interact: &InteractInfo) -> f32 { + if interact.active { + 5.0 + } else if interact.hovered { + 5.0 + } else { + 0.0 + } } /// Returns small icon rectangle and big icon rectangle