diff --git a/emigui/src/containers.rs b/emigui/src/containers.rs index 7886b7c2..6e685b93 100644 --- a/emigui/src/containers.rs +++ b/emigui/src/containers.rs @@ -1,6 +1,7 @@ pub mod area; pub mod collapsing_header; pub mod frame; +pub mod menu; pub mod resize; pub mod scroll_area; pub mod window; diff --git a/emigui/src/containers/menu.rs b/emigui/src/containers/menu.rs new file mode 100644 index 00000000..2a559c3a --- /dev/null +++ b/emigui/src/containers/menu.rs @@ -0,0 +1,140 @@ +use crate::{widgets::*, *}; + +use super::*; + +#[derive(Clone, Copy, Debug, serde_derive::Deserialize, serde_derive::Serialize)] +pub struct BarState { + #[serde(skip)] + open_menu: Option, + #[serde(skip)] + /// When did we open a menu? + open_time: f64, +} + +impl Default for BarState { + fn default() -> Self { + Self { + open_menu: None, + open_time: f64::NEG_INFINITY, + } + } +} + +pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) { + ui.horizontal(|ui| { + Frame::default().show(ui, |ui| { + let mut style = ui.style().clone(); + style.button_padding = vec2(2.0, 0.0); + style.interact.inactive.fill_color = None; + style.interact.inactive.outline = None; + style.interact.hovered.fill_color = None; + ui.set_style(style); + + // Take full width and fixed height: + ui.expand_to_size(vec2(ui.available_width(), ui.style().menu_bar.height)); + add_contents(ui) + }) + }) +} + +/// Construct a top level menu in a menu bar. This would be e.g. "File", "Edit" etc. +pub fn menu(ui: &mut Ui, title: impl Into, add_contents: impl FnOnce(&mut Ui)) { + let title = title.into(); + let bar_id = ui.id(); + let menu_id = Id::new(&title); + + let mut bar_state = ui + .memory() + .menu_bar + .get(&bar_id) + .cloned() + .unwrap_or_default(); + + let mut button = Button::new(title); + + if bar_state.open_menu == Some(menu_id) { + button = button.fill_color(ui.style().interact.active.fill_color); + } + + let button_interact = ui.add(button); + + interact_with_menu_button(&mut bar_state, ui.input(), menu_id, &button_interact); + + if bar_state.open_menu == Some(menu_id) { + let area = Area::new(menu_id) + .order(Order::Foreground) + .fixed_pos(button_interact.rect.left_bottom()); + let frame = Frame::menu(ui.style()); + + let resize = Resize::default().auto_sized(); + + let menu_interact = area.show(ui.ctx(), |ui| { + frame.show(ui, |ui| { + resize.show(ui, |ui| { + let mut style = ui.style().clone(); + style.button_padding = vec2(2.0, 0.0); + style.interact.inactive.fill_color = None; + style.interact.inactive.outline = None; + style.interact.active.corner_radius = 0.0; + style.interact.hovered.corner_radius = 0.0; + style.interact.inactive.corner_radius = 0.0; + ui.set_style(style); + ui.set_align(Align::Justified); + + add_contents(ui) + }) + }) + }); + + if menu_interact.hovered && ui.input().mouse_released { + bar_state.open_menu = None; + } + } + + ui.memory().menu_bar.insert(bar_id, bar_state); +} + +fn interact_with_menu_button( + bar_state: &mut BarState, + input: &GuiInput, + menu_id: Id, + button_interact: &GuiResponse, +) { + if button_interact.hovered && input.mouse_pressed { + if bar_state.open_menu.is_some() { + bar_state.open_menu = None; + } else { + bar_state.open_menu = Some(menu_id); + bar_state.open_time = input.time; + } + } + + if button_interact.hovered && input.mouse_released && bar_state.open_menu.is_some() { + let time_since_open = input.time - bar_state.open_time; + if time_since_open < 0.4 { + // A quick click + bar_state.open_menu = Some(menu_id); + bar_state.open_time = input.time; + } else { + // A long hold, then release + bar_state.open_menu = None; + } + } + + if button_interact.hovered && bar_state.open_menu.is_some() { + bar_state.open_menu = Some(menu_id); + } + + let pressed_escape = input.events.iter().any(|event| { + matches!( + event, + Event::Key { + key: Key::Escape, + pressed: true + } + ) + }); + if pressed_escape { + bar_state.open_menu = None; + } +} diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 75ae03de..9c8197cc 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -179,6 +179,8 @@ impl Widget for Hyperlink { pub struct Button { text: String, text_color: Option, + /// None means default for interact + fill_color: Option, } impl Button { @@ -186,6 +188,7 @@ impl Button { Self { text: text.into(), text_color: None, + fill_color: None, } } @@ -193,6 +196,11 @@ impl Button { self.text_color = Some(text_color); self } + + pub fn fill_color(mut self, fill_color: Option) -> Self { + self.fill_color = fill_color; + self + } } impl Widget for Button { @@ -207,9 +215,12 @@ impl Widget for Button { let interact = ui.reserve_space(size, Some(id)); let mut text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * text_size.y); text_cursor.y += 2.0; // TODO: why is this needed? + let fill_color = self + .fill_color + .or(ui.style().interact(&interact).fill_color); ui.add_paint_cmd(PaintCmd::Rect { corner_radius: ui.style().interact(&interact).corner_radius, - fill_color: ui.style().interact(&interact).fill_color, + fill_color: fill_color, outline: ui.style().interact(&interact).outline, rect: interact.rect, });