Rename foldable to CollapsingHeader and move to own file
This commit is contained in:
parent
0b61d952e6
commit
d70ae351c1
9 changed files with 216 additions and 155 deletions
|
@ -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.
|
||||
|
|
156
emigui/src/collapsing_header.rs
Normal file
156
emigui/src/collapsing_header.rs
Normal file
|
@ -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<String>) -> 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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<Id>,
|
||||
|
||||
// states of various types of widgets
|
||||
pub(crate) foldables: HashMap<Id, FoldableState>,
|
||||
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
|
||||
pub(crate) scroll_areas: HashMap<Id, ScrollState>,
|
||||
windows: HashMap<Id, WindowState>,
|
||||
|
||||
|
|
|
@ -143,118 +143,6 @@ impl Region {
|
|||
// ------------------------------------------------------------------------
|
||||
// Sub-regions:
|
||||
|
||||
pub fn foldable<S, F>(&mut self, text: S, add_contents: F) -> GuiResponse
|
||||
where
|
||||
S: Into<String>,
|
||||
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<F>(&mut self, id: Id, add_contents: F)
|
||||
where
|
||||
|
@ -453,6 +341,14 @@ impl Region {
|
|||
self.add(Label::new(text))
|
||||
}
|
||||
|
||||
pub fn collapsing<S, F>(&mut self, text: S, add_contents: F) -> GuiResponse
|
||||
where
|
||||
S: Into<String>,
|
||||
F: FnOnce(&mut Region),
|
||||
{
|
||||
CollapsingHeader::new(text).show(self, add_contents)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
pub fn reserve_space(&mut self, size: Vec2, interaction_id: Option<Id>) -> InteractInfo {
|
||||
|
|
|
@ -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<Outline> {
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue