1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

Rename foldable to CollapsingHeader and move to own file

This commit is contained in:
Emil Ernerfeldt
2020-04-22 18:15:27 +02:00
parent 0b61d952e6
commit d70ae351c1
9 changed files with 216 additions and 155 deletions

View 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 = &region.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,
});
}
}

View File

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

View File

@@ -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");
});

View File

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

View File

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

View File

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

View File

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

View File

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