Add button to collapse windows

This commit is contained in:
Emil Ernerfeldt 2020-05-17 12:26:17 +02:00
parent c79b28e3b0
commit f9bb9f71c4
8 changed files with 276 additions and 152 deletions

View file

@ -16,6 +16,7 @@ This is the core library crate Emigui. It is fully platform independent without
* [ ] Windows should open from `UI`s and be boxed by parent ui. * [ ] Windows should open from `UI`s and be boxed by parent ui.
* Then we could open the example app inside a window in the example app, recursively. * Then we could open the example app inside a window in the example app, recursively.
* [ ] Resize any side and corner on windows * [ ] Resize any side and corner on windows
* [ ] Fix autoshrink
* [ ] Scroll areas * [ ] Scroll areas
* [x] Vertical scrolling * [x] Vertical scrolling
* [ ] Horizontal scrolling * [ ] Horizontal scrolling
@ -29,7 +30,7 @@ This is the core library crate Emigui. It is fully platform independent without
* [ ] Text input * [ ] Text input
* [x] Input events (key presses) * [x] Input events (key presses)
* [x] Text focus * [x] Text focus
* [ ] Cursor movement * [x] Cursor movement
* [ ] Text selection * [ ] Text selection
* [ ] Clipboard copy/paste * [ ] Clipboard copy/paste
* [ ] Move focus with tab * [ ] Move focus with tab
@ -41,6 +42,7 @@ This is the core library crate Emigui. It is fully platform independent without
* [ ] Generalize Layout (separate from Ui) * [ ] Generalize Layout (separate from Ui)
* [ ] Cascading layout: same lite if it fits, else next line. Like text. * [ ] Cascading layout: same lite if it fits, else next line. Like text.
* [ ] Grid layout * [ ] Grid layout
* [ ] Point list
* [ ] Image support * [ ] Image support
### Web version: ### Web version:
@ -49,10 +51,12 @@ This is the core library crate Emigui. It is fully platform independent without
* [ ] Make it a JS library for easily creating your own stuff * [ ] Make it a JS library for easily creating your own stuff
* [ ] Read url fragment and redirect to a subpage (e.g. different examples apps) * [ ] Read url fragment and redirect to a subpage (e.g. different examples apps)
### Painting ### Visuals
* [ ] Pixel-perfect painting (round positions to nearest pixel). * [ ] Simplify button style to make for nicer collapsible headers. Maybe weak outline? Or just subtle different text color?
* [/] Pixel-perfect painting (round positions to nearest pixel).
* [ ] Make sure alpha blending is correct (different between web and glium) * [ ] Make sure alpha blending is correct (different between web and glium)
* [ ] sRGBA correct colors * [ ] Color picker widgets
* [ ] Fix thin rounded corners rendering bug (too bright)
### Animations ### Animations
Add extremely quick animations for some things, maybe 2-3 frames. For instance: Add extremely quick animations for some things, maybe 2-3 frames. For instance:
@ -94,6 +98,7 @@ Add extremely quick animations for some things, maybe 2-3 frames. For instance:
* [x] Combine Emigui and Context? * [x] Combine Emigui and Context?
* [x] Solve which parts of Context are behind a mutex * [x] Solve which parts of Context are behind a mutex
* [x] Rename Region to Ui * [x] Rename Region to Ui
* [ ] Move Path and Mesh to own crate
* [ ] Maybe find a shorter name for the library like `egui`? * [ ] Maybe find a shorter name for the library like `egui`?
### Global widget search ### Global widget search

View file

@ -22,6 +22,111 @@ impl Default for State {
} }
} }
impl State {
pub fn from_memory_with_default_open(ui: &Ui, id: Id, default_open: bool) -> Self {
ui.memory()
.collapsing_headers
.entry(id)
.or_insert(State {
open: default_open,
..Default::default()
})
.clone()
}
pub fn toggle(&mut self, ui: &Ui) {
self.open = !self.open;
self.toggle_time = ui.input().time;
}
/// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ui: &Ui) -> f32 {
let animation_time = ui.style().animation_time;
let time_since_toggle = (ui.input().time - self.toggle_time) as f32;
let time_since_toggle = time_since_toggle + ui.input().dt; // Instant feedback
if self.open {
remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0)
} else {
remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0)
}
}
/// Paint the arrow icon that indicated if the region is open or not
pub fn paint_icon(&self, ui: &mut Ui, interact: &InteractInfo) {
let stroke_color = ui.style().interact(interact).stroke_color;
let stroke_width = ui.style().interact(interact).stroke_width;
let rect = interact.rect;
let openness = self.openness(ui);
// Draw a pointy triangle arrow:
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()];
let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points {
let v = *p - rect.center();
let v = rotation.rotate_other(v);
*p = rect.center() + v;
}
ui.add_paint_cmd(PaintCmd::Path {
path: mesher::Path::from_point_loop(&points),
closed: true,
fill_color: None,
outline: Some(Outline::new(stroke_width, stroke_color)),
});
}
/// Show contents if we are open, with a nice animation between closed and open
pub fn add_contents(
&mut self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InteractInfo> {
let openness = self.openness(ui);
let animate = 0.0 < openness && openness < 1.0;
if animate {
Some(ui.add_custom(|child_ui| {
let max_height = if self.open {
if let Some(full_height) = self.open_height {
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
} else {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
// Just use a placehodler value that shows some movement:
10.0
}
} else {
let full_height = self.open_height.unwrap_or_default();
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
};
let mut clip_rect = child_ui.clip_rect();
clip_rect.max.y = clip_rect.max.y.min(child_ui.rect().top() + max_height);
child_ui.set_clip_rect(clip_rect);
let top_left = child_ui.top_left();
add_contents(child_ui);
self.open_height = Some(child_ui.bounding_size().y);
// Pretend children took up less space:
let mut child_bounds = child_ui.child_bounds();
child_bounds.max.y = child_bounds.max.y.min(top_left.y + max_height);
child_ui.force_set_child_bounds(child_bounds);
}))
} else if self.open {
let interact = ui.add_custom(add_contents);
let full_size = interact.rect.size();
self.open_height = Some(full_size.y);
Some(interact)
} else {
None
}
}
}
pub struct CollapsingHeader { pub struct CollapsingHeader {
label: Label, label: Label,
default_open: bool, default_open: bool,
@ -75,32 +180,25 @@ impl CollapsingHeader {
); );
let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0); let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0);
let mut state = { let mut state = State::from_memory_with_default_open(ui, id, default_open);
let mut memory = ui.memory(); if interact.clicked {
let mut state = memory.collapsing_headers.entry(id).or_insert(State { state.toggle(ui);
open: default_open, }
..Default::default()
});
if interact.clicked {
state.open = !state.open;
state.toggle_time = ui.input().time;
}
*state
};
let animation_time = ui.style().animation_time;
let time_since_toggle = (ui.input().time - state.toggle_time) as f32;
let time_since_toggle = time_since_toggle + ui.input().dt; // Instant feedback
let openness = if state.open {
remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0)
} else {
remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0)
};
let animate = time_since_toggle < animation_time;
let where_to_put_background = ui.paint_list_len(); let where_to_put_background = ui.paint_list_len();
paint_icon(ui, &interact, openness); {
let (mut icon_rect, _) = ui.style().icon_rectangles(interact.rect);
icon_rect.set_center(pos2(
interact.rect.left() + ui.style().indent / 2.0,
interact.rect.center().y,
));
let icon_interact = InteractInfo {
rect: icon_rect,
..interact
};
state.paint_icon(ui, &icon_interact);
}
ui.add_galley( ui.add_galley(
text_pos, text_pos,
@ -121,74 +219,11 @@ impl CollapsingHeader {
ui.expand_to_include_child(interact.rect); // TODO: remove, just a test ui.expand_to_include_child(interact.rect); // TODO: remove, just a test
if animate { state.add_contents(ui, |ui| {
ui.indent(id, |child_ui| { ui.indent(id, add_contents);
let max_height = if state.open { });
if let Some(full_height) = state.open_height {
remap(time_since_toggle, 0.0..=animation_time, 0.0..=full_height)
} else {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
// Just use a placehodler value that shows some movement:
10.0
}
} else {
let full_height = state.open_height.unwrap_or_default();
remap_clamp(time_since_toggle, 0.0..=animation_time, full_height..=0.0)
};
let mut clip_rect = child_ui.clip_rect();
clip_rect.max.y = clip_rect.max.y.min(child_ui.rect().top() + max_height);
child_ui.set_clip_rect(clip_rect);
let top_left = child_ui.top_left();
add_contents(child_ui);
state.open_height = Some(child_ui.bounding_size().y);
// Pretend children took up less space:
let mut child_bounds = child_ui.child_bounds();
child_bounds.max.y = child_bounds.max.y.min(top_left.y + max_height);
child_ui.force_set_child_bounds(child_bounds);
});
} else if state.open {
let full_size = ui.indent(id, add_contents).rect.size();
state.open_height = Some(full_size.y);
}
ui.memory().collapsing_headers.insert(id, state); ui.memory().collapsing_headers.insert(id, state);
ui.response(interact) ui.response(interact)
} }
} }
fn paint_icon(ui: &mut Ui, interact: &InteractInfo, openness: f32) {
let stroke_color = ui.style().interact(interact).stroke_color;
let stroke_width = ui.style().interact(interact).stroke_width;
let (mut small_icon_rect, _) = ui.style().icon_rectangles(interact.rect);
small_icon_rect.set_center(pos2(
interact.rect.left() + ui.style().indent / 2.0,
interact.rect.center().y,
));
// Draw a pointy triangle arrow:
let rect = Rect::from_center_size(
small_icon_rect.center(),
vec2(small_icon_rect.width(), small_icon_rect.height()) * 0.75,
);
let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()];
let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points {
let v = *p - rect.center();
let v = rotation.rotate_other(v);
*p = rect.center() + v;
}
// }
ui.add_paint_cmd(PaintCmd::Path {
path: mesher::Path::from_point_loop(&points),
closed: true,
fill_color: None,
outline: Some(Outline::new(stroke_width, stroke_color)),
});
}

View file

@ -122,7 +122,11 @@ impl<'open> Window<'open> {
} }
impl<'open> Window<'open> { impl<'open> Window<'open> {
pub fn show(self, ctx: &Arc<Context>, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo { pub fn show(
self,
ctx: &Arc<Context>,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InteractInfo> {
let Window { let Window {
title_label, title_label,
open, open,
@ -133,67 +137,110 @@ impl<'open> Window<'open> {
} = self; } = self;
if matches!(open, Some(false)) { if matches!(open, Some(false)) {
return Default::default(); return None;
} }
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
if true { Some(area.show(ctx, |ui| {
// TODO: easier way to compose these frame.show(ui, |ui| {
area.show(ctx, |ui| { let collapsing_id = ui.make_child_id("collapsing");
frame.show(ui, |ui| { let default_expanded = true;
let mut collapsing = collapsing_header::State::from_memory_with_default_open(
ui,
collapsing_id,
default_expanded,
);
let show_close_button = open.is_some();
let title_bar = show_title_bar(
ui,
title_label,
show_close_button,
collapsing_id,
&mut collapsing,
);
ui.memory()
.collapsing_headers
.insert(collapsing_id, collapsing);
let content = collapsing.add_contents(ui, |ui| {
resize.show(ui, |ui| { resize.show(ui, |ui| {
show_title_bar(ui, title_label, open); ui.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents
if let Some(scroll) = scroll { if let Some(scroll) = scroll {
scroll.show(ui, add_contents) scroll.show(ui, add_contents)
} else { } else {
add_contents(ui) add_contents(ui)
} }
}) })
}) });
if let Some(open) = open {
// Add close button now that we know our full width:
let right = content
.map(|c| c.rect.right())
.unwrap_or(title_bar.rect.right());
let button_size = ui.style().start_icon_width;
let button_rect = Rect::from_min_size(
pos2(
right - ui.style().item_spacing.x - button_size,
title_bar.rect.center().y - 0.5 * button_size,
),
Vec2::splat(button_size),
);
if close_button(ui, button_rect).clicked {
*open = false;
}
}
}) })
} else { }))
// TODO: something like this, with collapsing contents
area.show(ctx, |ui| {
frame.show(ui, |ui| {
CollapsingHeader::new(title_label.text()).show(ui, |ui| {
resize.show(ui, |ui| {
if let Some(scroll) = scroll {
scroll.show(ui, add_contents)
} else {
add_contents(ui)
}
})
});
})
})
}
} }
} }
fn show_title_bar(ui: &mut Ui, title_label: Label, open: Option<&mut bool>) { fn show_title_bar(
let button_size = ui.style().clickable_diameter; ui: &mut Ui,
title_label: Label,
show_close_button: bool,
collapsing_id: Id,
collapsing: &mut collapsing_header::State,
) -> InteractInfo {
ui.inner_layout(Layout::horizontal(Align::Center), |ui| {
ui.set_desired_height(title_label.font_height(ui));
// TODO: show collapse button let item_spacing = ui.style().item_spacing;
let button_size = ui.style().start_icon_width;
let title_rect = ui.add(title_label).rect; {
// TODO: make clickable radius larger
ui.reserve_space(vec2(0.0, 0.0), None); // HACK: will add left spacing
if let Some(open) = open { let collapse_button_interact =
let close_max_x = title_rect.right() + ui.style().item_spacing.x + button_size; ui.reserve_space(Vec2::splat(button_size), Some(collapsing_id));
let close_max_x = close_max_x.max(ui.rect_finite().right()); if collapse_button_interact.clicked {
let close_rect = Rect::from_min_size( // TODO: also do this when double-clicking window title
pos2( collapsing.toggle(ui);
close_max_x - button_size, }
title_rect.center().y - 0.5 * button_size, collapsing.paint_icon(ui, &collapse_button_interact);
),
Vec2::splat(button_size),
);
if close_button(ui, close_rect).clicked {
*open = false;
} }
}
ui.add(Separator::new().line_width(1.0)); // TODO: nicer way to split window title from contents let title_rect = ui.add(title_label).rect;
if show_close_button {
// Reserve space for close button which will be added later:
let close_max_x = title_rect.right() + item_spacing.x + button_size + item_spacing.x;
let close_max_x = close_max_x.max(ui.rect_finite().right());
let close_rect = Rect::from_min_size(
pos2(
close_max_x - button_size,
title_rect.center().y - 0.5 * button_size,
),
Vec2::splat(button_size),
);
ui.expand_to_include_child(close_rect);
}
})
} }
fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo { fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo {
@ -201,15 +248,6 @@ fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo {
let interact = ui.interact_rect(rect, close_id); let interact = ui.interact_rect(rect, close_id);
ui.expand_to_include_child(interact.rect); ui.expand_to_include_child(interact.rect);
// ui.add_paint_cmd(PaintCmd::Rect {
// corner_radius: ui.style().interact(&interact).corner_radius,
// fill_color: ui.style().interact(&interact).bg_fill_color,
// outline: ui.style().interact(&interact).rect_outline,
// rect: interact.rect,
// });
let rect = rect.expand(-4.0);
let stroke_color = ui.style().interact(&interact).stroke_color; let stroke_color = ui.style().interact(&interact).stroke_color;
let stroke_width = ui.style().interact(&interact).stroke_width; let stroke_width = ui.style().interact(&interact).stroke_width;
ui.add_paint_cmd(PaintCmd::line_segment( ui.add_paint_cmd(PaintCmd::line_segment(

View file

@ -297,7 +297,8 @@ impl Context {
rect: Rect, rect: Rect,
interaction_id: Option<Id>, interaction_id: Option<Id>,
) -> InteractInfo { ) -> InteractInfo {
let hovered = self.contains_mouse(layer, clip_rect, rect); let interact_rect = rect.expand2(0.5 * self.style().item_spacing); // make it easier to click. TODO: nice way to do this
let hovered = self.contains_mouse(layer, clip_rect, interact_rect);
let mut memory = self.memory(); let mut memory = self.memory();
let active = interaction_id.is_some() && memory.active_id == interaction_id; let active = interaction_id.is_some() && memory.active_id == interaction_id;

View file

@ -42,7 +42,7 @@ impl Default for CursorIcon {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, Default, Serialize)] #[derive(Clone, Copy, Debug, Serialize)]
pub struct InteractInfo { pub struct InteractInfo {
/// The mouse is hovering above this thing /// The mouse is hovering above this thing
pub hovered: bool, pub hovered: bool,
@ -58,6 +58,15 @@ pub struct InteractInfo {
} }
impl InteractInfo { impl InteractInfo {
pub fn nothing() -> Self {
Self {
hovered: false,
clicked: false,
active: false,
rect: Rect::nothing(),
}
}
pub fn union(self, other: Self) -> Self { pub fn union(self, other: Self) -> Self {
Self { Self {
hovered: self.hovered || other.hovered, hovered: self.hovered || other.hovered,

View file

@ -339,7 +339,16 @@ impl Ui {
/// for `Justified` aligned layouts, like in menus. /// for `Justified` aligned layouts, like in menus.
/// ///
/// You may get LESS space than you asked for if the current layout won't fit what you asked for. /// You may get LESS space than you asked for if the current layout won't fit what you asked for.
///
/// TODO: remove, or redesign or something and start using allocate_space
pub fn reserve_space(&mut self, child_size: Vec2, interaction_id: Option<Id>) -> InteractInfo { pub fn reserve_space(&mut self, child_size: Vec2, interaction_id: Option<Id>) -> InteractInfo {
let rect = self.allocate_space(child_size);
self.ctx
.interact(self.layer, self.clip_rect, rect, interaction_id)
}
pub fn allocate_space(&mut self, child_size: Vec2) -> Rect {
let child_size = self.round_vec_to_pixels(child_size); let child_size = self.round_vec_to_pixels(child_size);
self.cursor = self.round_pos_to_pixels(self.cursor); self.cursor = self.round_pos_to_pixels(self.cursor);
@ -376,8 +385,7 @@ impl Ui {
} }
} }
self.ctx rect
.interact(self.layer, self.clip_rect, rect, interaction_id)
} }
/// Reserve this much space and move the cursor. /// Reserve this much space and move the cursor.
@ -517,12 +525,25 @@ impl Ui {
/// Just because you ask for a lot of space does not mean you have to use it! /// Just because you ask for a lot of space does not mean you have to use it!
/// After `add_contents` is called the contents of `bounding_size` /// After `add_contents` is called the contents of `bounding_size`
/// will decide how much space will be used in the parent ui. /// will decide how much space will be used in the parent ui.
pub fn add_custom_contents(&mut self, size: Vec2, add_contents: impl FnOnce(&mut Ui)) { pub fn add_custom_contents(
&mut self,
size: Vec2,
add_contents: impl FnOnce(&mut Ui),
) -> InteractInfo {
let size = size.min(self.available().size()); let size = size.min(self.available().size());
let child_rect = Rect::from_min_size(self.cursor, size); let child_rect = Rect::from_min_size(self.cursor, size);
let mut child_ui = self.child_ui(child_rect); let mut child_ui = self.child_ui(child_rect);
add_contents(&mut child_ui); add_contents(&mut child_ui);
self.reserve_space(child_ui.bounding_size(), None); self.reserve_space(child_ui.bounding_size(), None)
}
/// Create a child ui
pub fn add_custom(&mut self, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo {
let child_rect = self.available();
let mut child_ui = self.child_ui(child_rect);
add_contents(&mut child_ui);
let size = child_ui.bounding_size();
self.reserve_space(size, None)
} }
/// Create a child ui which is indented to the right /// Create a child ui which is indented to the right

View file

@ -72,6 +72,10 @@ impl Label {
} }
} }
pub fn font_height(&self, ui: &Ui) -> f32 {
ui.fonts()[self.text_style].height()
}
// TODO: this should return a LabelLayout which has a paint method. // TODO: this should return a LabelLayout which has a paint method.
// We can then split Widget::Ui in two: layout + allocating space, and painting. // We can then split Widget::Ui in two: layout + allocating space, and painting.
// this allows us to assemble lables, THEN detect interaction, THEN chose color style based on that. // this allows us to assemble lables, THEN detect interaction, THEN chose color style based on that.

View file

@ -62,8 +62,10 @@ fn main() {
let pixels_per_point = display.gl_window().get_hidpi_factor() as f32; let pixels_per_point = display.gl_window().get_hidpi_factor() as f32;
let mut ctx = Context::new(pixels_per_point); let mut ctx = profile("initializing emilib", || Context::new(pixels_per_point));
let mut painter = emigui_glium::Painter::new(&display); let mut painter = profile("initializing painter", || {
emigui_glium::Painter::new(&display)
});
let mut raw_input = emigui::RawInput { let mut raw_input = emigui::RawInput {
screen_size: { screen_size: {
@ -143,6 +145,7 @@ fn main() {
emigui_glium::handle_output(output, &display, clipboard.as_mut()); emigui_glium::handle_output(output, &display, clipboard.as_mut());
} }
// Save state to disk:
window_settings.pos = display window_settings.pos = display
.gl_window() .gl_window()
.get_position() .get_position()
@ -162,3 +165,11 @@ fn main() {
) )
.unwrap(); .unwrap();
} }
fn profile<R>(name: &str, action: impl FnOnce() -> R) -> R {
let start = Instant::now();
let r = action();
let elapsed = start.elapsed();
eprintln!("{}: {} ms", name, elapsed.as_millis());
r
}