From 01aea23388fbea3e5752b771ba982bc38a970a5f Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 00:15:31 +0200 Subject: [PATCH 01/46] Only drag with primary pointer button --- egui/src/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index 6b9130c11a25..2c3575a1ff9d 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -249,7 +249,7 @@ impl CtxRef { // the slider will steal the drag away from the window. // This is needed because we do window interaction first (to prevent frame delay), // and then do content layout. - if sense.drag + if sense.drag && self.input().pointer.button_down(PointerButton::Primary) && (memory.interaction.drag_id.is_none() || memory.interaction.drag_is_window) { From 04e52be74e15aaac3b854ac10c8969dd7a9d9dc3 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Sat, 3 Jul 2021 16:46:30 +0200 Subject: [PATCH 02/46] Add ContextMenuSystem for context menu Listens for secondary click, support for sub-menus --- egui/src/context_menu.rs | 220 ++++++++++++++++++++++++++++++++++ egui/src/lib.rs | 1 + egui_demo_lib/src/wrap_app.rs | 35 ++++++ 3 files changed, 256 insertions(+) create mode 100644 egui/src/context_menu.rs diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs new file mode 100644 index 000000000000..d8a5f433a088 --- /dev/null +++ b/egui/src/context_menu.rs @@ -0,0 +1,220 @@ +use super::{ + Color32, Id, Rect, Ui, + Frame, Area, + Response, CtxRef, + Pos2, Order, + Align, Layout, + PointerButton, +}; + +#[derive(Default, Clone)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "persistence", serde(default))] +pub struct ContextMenuSystem { + context_menu: Option, +} +impl ContextMenuSystem { + pub fn listen(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + let pointer = &ctx.input().pointer; + if let Some(pos) = pointer.interact_pos() { + let mut destroy = false; + let mut reset = true; + if let Some(context_menu) = &mut self.context_menu { + let response = context_menu.show_root(ctx, add_contents); + context_menu.state.rect = response.rect; + + if context_menu.state.response.is_close() { + destroy = true; + } + if !pointer.any_pressed() || context_menu.area_contains(pos) { + reset = false; + } + } + if reset && pointer.button_down(PointerButton::Secondary) { + // todo: adapt to context + self.context_menu = Some(ContextMenu::new(pos)); + } else if destroy || (reset && pointer.button_down(PointerButton::Primary)) { + self.context_menu = None; + } + } + } +} +#[derive(Clone)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +enum MenuResponse { + Close, + Stay, +} +impl MenuResponse { + pub fn is_close(&self) -> bool { + match self { + &Self::Close => true, + _ => false, + } + } +} +#[derive(Default, Clone)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "persistence", serde(default))] +struct ContextMenu { + state: MenuState, + position: Pos2, +} +impl ContextMenu { + pub fn new(position: Pos2) -> Self { + Self { + state: MenuState::default(), + position, + } + } + pub fn sub_menu(position: Pos2, state: MenuState) -> Self { + Self { + state, + position, + } + } + fn show_impl(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) -> Response { + Area::new(format!("context_menu_{:#?}", self.position)) + .order(Order::Foreground) + .fixed_pos(self.position.to_vec2()) + .interactable(true) + .show(ctx, |ui| { + Frame::none() + .fill(Color32::BLACK) + .corner_radius(3.0) + .margin((0.0, 3.0)) + .show(ui, |ui| + ui.with_layout( + Layout::top_down_justified(Align::LEFT), + add_contents, + ) + ); + }) + } + pub(crate) fn show_root(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { + let mut state = self.state.clone(); + let response = self.show_impl(ctx, |ui| add_contents(ui, &mut state)); + self.state = state; + response + } + pub fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) -> Response { + self.show_impl(ctx, add_contents) + } +} +impl std::ops::Deref for ContextMenu { + type Target = MenuState; + fn deref(&self) -> &Self::Target { + &self.state + } +} +impl std::ops::DerefMut for ContextMenu { + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.state + } +} +#[derive(Clone)] +pub struct SubMenu { + text: String, +} +impl SubMenu { + pub fn new(text: impl ToString) -> Self { + Self { + text: text.to_string(), + } + } + pub fn show(self, ui: &mut Ui, parent_state: &mut MenuState, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { + let button = ui.button(self.text); + let mut sub_hovered = false; + if let Some(sub_menu) = parent_state.get_submenu(button.id) { + if let Some(pos) = ui.input().pointer.hover_pos() { + sub_hovered = sub_menu.area_contains(pos); + } + } + if !sub_hovered { + if button.hovered() { + parent_state.open_submenu(button.id); + } else { + parent_state.close_submenu(button.id); + } + } + let responses = parent_state.get_submenu(button.id).map(|menu_state| { + let response = ContextMenu::sub_menu(button.rect.right_top(), menu_state.clone()) + .show(ui.ctx(), |ui| add_contents(ui, menu_state)); + // set submenu bounding box + menu_state.rect = response.rect; + (menu_state.response.clone(), response) + }); + if let Some((menu_response, response)) = responses { + parent_state.cascade_response(menu_response); + response + } else { + button + } + } +} + +#[derive(Clone)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "persistence", serde(default))] +pub struct MenuState { + sub_menu: Option<(Id, Box)>, + pub rect: Rect, + response: MenuResponse, +} +impl Default for MenuState { + fn default() -> Self { + Self { + rect: Rect::NOTHING, + sub_menu: None, + response: MenuResponse::Stay + } + } +} +#[allow(unused)] +impl MenuState { + pub(crate) fn area_contains(&self, pos: Pos2) -> bool{ + self.rect.contains(pos) || + self.sub_menu.as_ref() + .map(|(_, sub)| sub.area_contains(pos)) + .unwrap_or(false) + } + pub fn close(&mut self) { + self.response = MenuResponse::Close; + } + fn cascade_response(&mut self, response: MenuResponse) { + if response.is_close() { + self.response = response; + } + } + pub fn get_submenu(&mut self, id: Id) -> Option<&mut MenuState> { + self.sub_menu.as_mut().and_then(|(k, sub)| if id == *k { + Some(sub.as_mut()) + } else { + None + }) + } + fn open_submenu(&mut self, id: Id) { + if let Some((k, _)) = self.sub_menu { + if k == id { + return; + } + } + self.sub_menu = Some((id, Box::new(MenuState::default()))); + } + fn close_submenu(&mut self, id: Id) { + if let Some((k, _)) = self.sub_menu { + if k == id { + self.sub_menu = None; + } + } + } + pub fn toggle_submenu(&mut self, id: Id) { + if let Some((k, _)) = self.sub_menu.take() { + if k == id { + self.sub_menu = None; + return; + } + } + self.sub_menu = Some((id, Box::new(MenuState::default()))); + } +} \ No newline at end of file diff --git a/egui/src/lib.rs b/egui/src/lib.rs index f737646d590d..72402d3a4ccb 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -369,6 +369,7 @@ pub mod style; mod ui; pub mod util; pub mod widgets; +pub mod context_menu; pub use epaint; pub use epaint::emath; diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 5a62f3baa7cf..3f2e7f8484b1 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -73,6 +73,8 @@ impl epi::App for WrapApp { } fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { + self.context_menu(ctx, frame); + if let Some(web_info) = frame.info().web_info.as_ref() { if let Some(anchor) = web_info.web_location_hash.strip_prefix('#') { self.selected_anchor = anchor.to_owned(); @@ -126,6 +128,39 @@ impl epi::App for WrapApp { } impl WrapApp { + fn context_menu(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { + use egui::context_menu::SubMenu; + self.context_menu.listen(ctx, |ui, menu_state| { + let open_button = ui.button("Open..."); + if open_button.clicked() { + menu_state.close(); + } + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, menu_state| { + let open_button = ui.button("Open..."); + if open_button.clicked() { + menu_state.close(); + } + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, menu_state| { + let open_button = ui.button("Open..."); + if open_button.clicked() { + menu_state.close(); + } + let _ = ui.button("Item"); + }); + let _ = ui.button("Item"); + }); + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, _menu_state| { + let _ = ui.button("Item1"); + let _ = ui.button("Item2"); + let _ = ui.button("Item3"); + let _ = ui.button("Item4"); + }); + let _ = ui.button("Item"); + }); + } fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { // A menu-bar is a horizontal layout with some special styles applied. // egui::menu::bar(ui, |ui| { From 432ac9cf3c3ac433b7f2d5295fd71ee0cb62ca4e Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Sat, 3 Jul 2021 17:22:44 +0200 Subject: [PATCH 03/46] Syntax simplification in demo context menu builder --- egui_demo_lib/src/wrap_app.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 3f2e7f8484b1..8a2fad3d5b3a 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -131,20 +131,17 @@ impl WrapApp { fn context_menu(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { use egui::context_menu::SubMenu; self.context_menu.listen(ctx, |ui, menu_state| { - let open_button = ui.button("Open..."); - if open_button.clicked() { + if ui.button("Open...").clicked() { menu_state.close(); } SubMenu::new("SubMenu") .show(ui, menu_state, |ui, menu_state| { - let open_button = ui.button("Open..."); - if open_button.clicked() { + if ui.button("Open...").clicked() { menu_state.close(); } SubMenu::new("SubMenu") .show(ui, menu_state, |ui, menu_state| { - let open_button = ui.button("Open..."); - if open_button.clicked() { + if ui.button("Open...").clicked() { menu_state.close(); } let _ = ui.button("Item"); From 185d86e5223a1e5e23fb342df82336eb6471883d Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Tue, 6 Jul 2021 22:20:56 +0200 Subject: [PATCH 04/46] Store ContextMenuSystem in Context and respect Ui clicked --- egui/src/context.rs | 15 +++ egui/src/context_menu.rs | 94 +++++++++++++------ egui/src/ui.rs | 5 + egui_demo_lib/src/apps/demo/drag_and_drop.rs | 18 ++-- egui_demo_lib/src/apps/demo/window_options.rs | 7 +- egui_demo_lib/src/backend_panel.rs | 32 +++++++ egui_demo_lib/src/wrap_app.rs | 9 +- 7 files changed, 142 insertions(+), 38 deletions(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index 2c3575a1ff9d..a67c1f5a76a4 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -12,6 +12,7 @@ use crate::{ input_state::*, layers::GraphicLayers, mutex::{Mutex, MutexGuard}, + context_menu::{ ContextMenuSystem, MenuState }, *, }; use epaint::{stats::*, text::Fonts, *}; @@ -303,6 +304,11 @@ impl CtxRef { pub fn debug_painter(&self) -> Painter { Self::layer_painter(self, LayerId::debug()) } + + + pub(crate) fn show_ui_context_menu(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + self.context_menu_system().ui_context_menu(ui, add_contents) + } } // ---------------------------------------------------------------------------- @@ -329,9 +335,11 @@ pub struct Context { fonts: Option>, memory: Arc>, animation_manager: Arc>, + context_menu_system: Arc>, input: InputState, + /// State that is collected during a frame and then cleared frame_state: Arc>, @@ -357,6 +365,7 @@ impl Clone for Context { output: self.output.clone(), paint_stats: self.paint_stats.clone(), repaint_requests: self.repaint_requests.load(SeqCst).into(), + context_menu_system: self.context_menu_system.clone(), } } } @@ -375,6 +384,12 @@ impl Context { self.memory.lock() } + /// Stores all the egui state. + /// If you want to store/restore egui, serialize this. + pub fn context_menu_system(&self) -> MutexGuard<'_, ContextMenuSystem> { + self.context_menu_system.lock() + } + pub(crate) fn graphics(&self) -> MutexGuard<'_, GraphicLayers> { self.graphics.lock() } diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index d8a5f433a088..0bf5d21540ce 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -8,60 +8,98 @@ use super::{ }; #[derive(Default, Clone)] -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "persistence", serde(default))] pub struct ContextMenuSystem { - context_menu: Option, + context_menu: Option, } impl ContextMenuSystem { - pub fn listen(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { - let pointer = &ctx.input().pointer; - if let Some(pos) = pointer.interact_pos() { - let mut destroy = false; - let mut reset = true; - if let Some(context_menu) = &mut self.context_menu { - let response = context_menu.show_root(ctx, add_contents); - context_menu.state.rect = response.rect; + fn response(&mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> MenuResponse { + if let Some(context_menu) = &mut self.context_menu { + if context_menu.ui_id == ui.id() { + let response = context_menu.show(ui.ctx(), add_contents); + context_menu.rect = response.rect; - if context_menu.state.response.is_close() { - destroy = true; - } - if !pointer.any_pressed() || context_menu.area_contains(pos) { - reset = false; + if context_menu.response.is_close() { + return MenuResponse::Close; } } - if reset && pointer.button_down(PointerButton::Secondary) { - // todo: adapt to context - self.context_menu = Some(ContextMenu::new(pos)); - } else if destroy || (reset && pointer.button_down(PointerButton::Primary)) { - self.context_menu = None; + } + let pointer = &ui.input().pointer; + if let Some(pos) = pointer.interact_pos() { + if pointer.any_pressed() { + let mut destroy = false; + if let Some(context_menu) = &mut self.context_menu { + let in_old_menu = context_menu.area_contains(pos); + destroy = !in_old_menu && context_menu.ui_id == ui.id(); + } + let in_ui = ui.rect_contains_pointer(ui.max_rect_finite()); + if in_ui { + if pointer.button_down(PointerButton::Secondary) { + // todo: adapt to context + return MenuResponse::Create(pos); + } else { + return MenuResponse::Close; + } + } else if destroy { + return MenuResponse::Close; + } } } + MenuResponse::Stay + } + pub fn ui_context_menu(&mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + match self.response(ui, add_contents) { + MenuResponse::Create(pos) => self.context_menu = Some(ContextMenuRoot::new(pos, ui.id())), + MenuResponse::Close => self.context_menu = None, + MenuResponse::Stay => {} + } } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] enum MenuResponse { Close, Stay, + Create(Pos2), } impl MenuResponse { pub fn is_close(&self) -> bool { - match self { - &Self::Close => true, - _ => false, + *self == Self::Close + } +} +#[derive(Clone)] +struct ContextMenuRoot { + context_menu: ContextMenu, + ui_id: Id, +} +impl ContextMenuRoot { + pub fn new(position: Pos2, ui_id: Id) -> Self { + Self { + context_menu: ContextMenu::root(position), + ui_id, } } + pub(crate) fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { + self.context_menu.show_root(ctx, add_contents) + } +} +impl std::ops::Deref for ContextMenuRoot { + type Target = MenuState; + fn deref(&self) -> &Self::Target { + &self.context_menu.state + } +} +impl std::ops::DerefMut for ContextMenuRoot { + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.context_menu.state + } } #[derive(Default, Clone)] -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "persistence", serde(default))] struct ContextMenu { state: MenuState, position: Pos2, } impl ContextMenu { - pub fn new(position: Pos2) -> Self { + pub fn root(position: Pos2) -> Self { Self { state: MenuState::default(), position, diff --git a/egui/src/ui.rs b/egui/src/ui.rs index acf6d4d05771..043fb7589114 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -4,6 +4,7 @@ use std::hash::Hash; use crate::{ color::*, containers::*, epaint::text::Fonts, layout::*, mutex::MutexGuard, placer::Placer, + context_menu::MenuState, widgets::*, *, }; @@ -342,6 +343,10 @@ impl Ui { pub fn set_clip_rect(&mut self, clip_rect: Rect) { self.painter.set_clip_rect(clip_rect); } + + pub fn context_menu(&mut self, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + self.ctx().clone().show_ui_context_menu(self, add_contents) + } } // ------------------------------------------------------------------------ diff --git a/egui_demo_lib/src/apps/demo/drag_and_drop.rs b/egui_demo_lib/src/apps/demo/drag_and_drop.rs index 2d272511eb59..5b4679c1d23d 100644 --- a/egui_demo_lib/src/apps/demo/drag_and_drop.rs +++ b/egui_demo_lib/src/apps/demo/drag_and_drop.rs @@ -78,7 +78,7 @@ pub fn drop_target( pub struct DragAndDropDemo { /// columns with items - columns: Vec>, + columns: Vec>, } impl Default for DragAndDropDemo { @@ -88,7 +88,9 @@ impl Default for DragAndDropDemo { vec!["Item A", "Item B", "Item C"], vec!["Item D", "Item E"], vec!["Item F", "Item G", "Item H"], - ], + ].into_iter() + .map(|v| v.into_iter().map(ToString::to_string).collect()) + .collect(), } } } @@ -116,15 +118,19 @@ impl super::View for DragAndDropDemo { let mut source_col_row = None; let mut drop_col = None; - ui.columns(self.columns.len(), |uis| { - for (col_idx, column) in self.columns.iter().enumerate() { + uis[0].context_menu(|ui, menu_state| { + if ui.button("New Item...").clicked() { + self.columns[0].push("New Item".to_string()); + menu_state.close(); + } + }); + for (col_idx, column) in self.columns.clone().into_iter().enumerate() { let ui = &mut uis[col_idx]; let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯ let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| { ui.set_min_size(vec2(64.0, 100.0)); - - for (row_idx, &item) in column.iter().enumerate() { + for (row_idx, item) in column.iter().enumerate() { let item_id = Id::new("item").with(col_idx).with(row_idx); drag_source(ui, item_id, |ui| { ui.label(item); diff --git a/egui_demo_lib/src/apps/demo/window_options.rs b/egui_demo_lib/src/apps/demo/window_options.rs index 8bc919ec9aa0..cd09a83c8ada 100644 --- a/egui_demo_lib/src/apps/demo/window_options.rs +++ b/egui_demo_lib/src/apps/demo/window_options.rs @@ -87,10 +87,15 @@ impl super::View for WindowOptions { anchor, anchor_offset, } = self; - ui.horizontal(|ui| { ui.label("title:"); ui.text_edit_singleline(title); + ui.context_menu(|ui, menu_state| { + if ui.button("Clear..").clicked() { + *title = String::new(); + menu_state.close(); + } + }); }); ui.horizontal(|ui| { diff --git a/egui_demo_lib/src/backend_panel.rs b/egui_demo_lib/src/backend_panel.rs index c5b25039abcb..59897a088fb7 100644 --- a/egui_demo_lib/src/backend_panel.rs +++ b/egui_demo_lib/src/backend_panel.rs @@ -92,8 +92,40 @@ impl BackendPanel { self.egui_windows.windows(ctx); } + fn context_menu(&mut self, ui: &mut egui::Ui) { + use egui::context_menu::SubMenu; + ui.context_menu(|ui, menu_state| { + if ui.button("Open...").clicked() { + menu_state.close(); + } + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, menu_state| { + if ui.button("Open...").clicked() { + menu_state.close(); + } + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, menu_state| { + if ui.button("Open...").clicked() { + menu_state.close(); + } + let _ = ui.button("Item"); + }); + let _ = ui.button("Item"); + }); + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, _menu_state| { + let _ = ui.button("Item1"); + let _ = ui.button("Item2"); + let _ = ui.button("Item3"); + let _ = ui.button("Item4"); + }); + let _ = ui.button("Item"); + }); + } + pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { egui::trace!(ui); + self.context_menu(ui); ui.vertical_centered(|ui| { ui.heading("💻 Backend"); }); diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 8a2fad3d5b3a..3f2e7f8484b1 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -131,17 +131,20 @@ impl WrapApp { fn context_menu(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { use egui::context_menu::SubMenu; self.context_menu.listen(ctx, |ui, menu_state| { - if ui.button("Open...").clicked() { + let open_button = ui.button("Open..."); + if open_button.clicked() { menu_state.close(); } SubMenu::new("SubMenu") .show(ui, menu_state, |ui, menu_state| { - if ui.button("Open...").clicked() { + let open_button = ui.button("Open..."); + if open_button.clicked() { menu_state.close(); } SubMenu::new("SubMenu") .show(ui, menu_state, |ui, menu_state| { - if ui.button("Open...").clicked() { + let open_button = ui.button("Open..."); + if open_button.clicked() { menu_state.close(); } let _ = ui.button("Item"); From 69a33d80ea099ca564178794e34b2a00a7a395b9 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Tue, 6 Jul 2021 23:03:09 +0200 Subject: [PATCH 05/46] Add context menu for widget_gallery plot --- egui_demo_lib/src/apps/demo/widget_gallery.rs | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index 5d280a94c5e2..df4d47ec800d 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -6,6 +6,14 @@ enum Enum { Third, } +#[derive(Clone, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +enum Plot { + Sin, + Bell, + Sigmoid, +} + /// Shows off one example of each major type of widget. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct WidgetGallery { @@ -13,6 +21,7 @@ pub struct WidgetGallery { visible: bool, boolean: bool, radio: Enum, + plot: Plot, scalar: f32, string: String, color: egui::Color32, @@ -30,6 +39,7 @@ impl Default for WidgetGallery { string: Default::default(), color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5), animate_progress_bar: false, + plot: Plot::Sin, } } } @@ -99,6 +109,7 @@ impl WidgetGallery { string, color, animate_progress_bar, + plot, } = self; ui.add(doc_link_label("Label", "label,heading")); @@ -211,7 +222,22 @@ impl WidgetGallery { ui.end_row(); ui.add(doc_link_label("Plot", "plot")); - ui.add(example_plot()); + ui.scope(|ui| { + ui.context_menu(|ui, menu_state| { + if ui.button("Sin").clicked() { + *plot = Plot::Sin; + menu_state.close(); + } else if ui.button("Bell").clicked() { + *plot = Plot::Bell; + menu_state.close(); + } else if ui.button("Sigmoid").clicked() { + *plot = Plot::Sigmoid; + menu_state.close(); + } + }); + ui.add(example_plot(plot)); + }); + ui.end_row(); ui.hyperlink_to( @@ -225,21 +251,32 @@ impl WidgetGallery { ui.end_row(); } } - -fn example_plot() -> egui::plot::Plot { - use egui::plot::{Line, Plot, Value, Values}; +fn gaussian(x: f64) -> f64 { + let var: f64 = 2.0; + f64::exp(-0.5*(x/var).powi(2))/(var*f64::sqrt(std::f64::consts::TAU)) +} +fn sigmoid(x: f64) -> f64 { + 1.0/(1.0 + f64::exp(-x)) +} +fn example_plot(plot: &Plot) -> egui::plot::Plot { + use egui::plot::{Line, Value, Values}; let n = 128; let line = Line::new(Values::from_values_iter((0..=n).map(|i| { use std::f64::consts::TAU; - let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU); - Value::new(x, x.sin()) + let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU); + match plot { + Plot::Sin => Value::new(x, x.sin()), + Plot::Bell => Value::new(x, 5.0*gaussian(x)), + Plot::Sigmoid => Value::new(x, sigmoid(x)), + } }))); - Plot::new("example_plot") + egui::plot::Plot::new("example_plot") .line(line) .height(32.0) .data_aspect(1.0) } + fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a { let label = format!("{}:", title); let url = format!("https://docs.rs/egui?search={}", search_term); From 28ef15d04e097dc3f6a130cbd4093c7c8d3986ba Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 00:14:54 +0200 Subject: [PATCH 06/46] Fix menu being reset when clicking on it --- egui/src/context_menu.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 0bf5d21540ce..92e6bcd10aaa 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -27,20 +27,23 @@ impl ContextMenuSystem { if let Some(pos) = pointer.interact_pos() { if pointer.any_pressed() { let mut destroy = false; + let mut in_old_menu = false; if let Some(context_menu) = &mut self.context_menu { - let in_old_menu = context_menu.area_contains(pos); - destroy = !in_old_menu && context_menu.ui_id == ui.id(); + in_old_menu = context_menu.area_contains(pos); + destroy = context_menu.ui_id == ui.id(); } let in_ui = ui.rect_contains_pointer(ui.max_rect_finite()); - if in_ui { - if pointer.button_down(PointerButton::Secondary) { - // todo: adapt to context - return MenuResponse::Create(pos); - } else { + if !in_old_menu { + if in_ui { + if pointer.button_down(PointerButton::Secondary) { + // todo: adapt to context + return MenuResponse::Create(pos); + } else { + return MenuResponse::Close; + } + } else if destroy { return MenuResponse::Close; } - } else if destroy { - return MenuResponse::Close; } } } @@ -48,8 +51,12 @@ impl ContextMenuSystem { } pub fn ui_context_menu(&mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { match self.response(ui, add_contents) { - MenuResponse::Create(pos) => self.context_menu = Some(ContextMenuRoot::new(pos, ui.id())), - MenuResponse::Close => self.context_menu = None, + MenuResponse::Create(pos) => { + self.context_menu = Some(ContextMenuRoot::new(pos, ui.id())); + }, + MenuResponse::Close => { + self.context_menu = None + }, MenuResponse::Stay => {} } } From 1817b62354c6c2ef9c24e826bfb30b79c4e4576f Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 00:48:30 +0200 Subject: [PATCH 07/46] Adapt to removal of From conversions Vec2 <-> Pos2 --- egui/src/context_menu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 92e6bcd10aaa..6e70882c946b 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -121,7 +121,7 @@ impl ContextMenu { fn show_impl(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) -> Response { Area::new(format!("context_menu_{:#?}", self.position)) .order(Order::Foreground) - .fixed_pos(self.position.to_vec2()) + .fixed_pos(self.position) .interactable(true) .show(ctx, |ui| { Frame::none() @@ -262,4 +262,4 @@ impl MenuState { } self.sub_menu = Some((id, Box::new(MenuState::default()))); } -} \ No newline at end of file +} From 7011c371d868efe630dae95db828e73c5af3ce87 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 15:57:39 +0200 Subject: [PATCH 08/46] Apply context menu to Response --- egui/src/context.rs | 4 +- egui/src/context_menu.rs | 50 ++++++++++------- egui/src/response.rs | 5 ++ egui/src/ui.rs | 3 - egui_demo_lib/src/apps/demo/drag_and_drop.rs | 23 +++++--- egui_demo_lib/src/apps/demo/widget_gallery.rs | 6 +- egui_demo_lib/src/apps/demo/window_options.rs | 13 +++-- egui_demo_lib/src/backend_panel.rs | 55 +++++++++---------- egui_demo_lib/src/wrap_app.rs | 11 +++- 9 files changed, 98 insertions(+), 72 deletions(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index a67c1f5a76a4..268d7358bebf 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -306,8 +306,8 @@ impl CtxRef { } - pub(crate) fn show_ui_context_menu(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { - self.context_menu_system().ui_context_menu(ui, add_contents) + pub(crate) fn show_ui_context_menu(&self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + self.context_menu_system().ui_context_menu(response, add_contents) } } diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 6e70882c946b..8e5d475cc562 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -12,29 +12,23 @@ pub struct ContextMenuSystem { context_menu: Option, } impl ContextMenuSystem { - fn response(&mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> MenuResponse { - if let Some(context_menu) = &mut self.context_menu { - if context_menu.ui_id == ui.id() { - let response = context_menu.show(ui.ctx(), add_contents); - context_menu.rect = response.rect; - - if context_menu.response.is_close() { - return MenuResponse::Close; - } - } - } - let pointer = &ui.input().pointer; - if let Some(pos) = pointer.interact_pos() { - if pointer.any_pressed() { + fn sense_click(&mut self, response: &Response) -> MenuResponse { + let Response { + id, + ctx, + .. + } = response; + let pointer = &ctx.input().pointer; + if pointer.any_pressed() { + if let Some(pos) = pointer.interact_pos() { let mut destroy = false; let mut in_old_menu = false; if let Some(context_menu) = &mut self.context_menu { in_old_menu = context_menu.area_contains(pos); - destroy = context_menu.ui_id == ui.id(); + destroy = context_menu.ui_id == *id; } - let in_ui = ui.rect_contains_pointer(ui.max_rect_finite()); if !in_old_menu { - if in_ui { + if response.hovered() { if pointer.button_down(PointerButton::Secondary) { // todo: adapt to context return MenuResponse::Create(pos); @@ -49,15 +43,31 @@ impl ContextMenuSystem { } MenuResponse::Stay } - pub fn ui_context_menu(&mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { - match self.response(ui, add_contents) { + fn show(&mut self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> MenuResponse { + if let Some(context_menu) = &mut self.context_menu { + if context_menu.ui_id == response.id { + let response = context_menu.show(&response.ctx, add_contents); + context_menu.rect = response.rect; + + if context_menu.response.is_close() { + return MenuResponse::Close; + } + } + } + MenuResponse::Stay + } + pub fn ui_context_menu(&mut self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + match self.sense_click(response) { MenuResponse::Create(pos) => { - self.context_menu = Some(ContextMenuRoot::new(pos, ui.id())); + self.context_menu = Some(ContextMenuRoot::new(pos, response.id)); }, MenuResponse::Close => { self.context_menu = None }, MenuResponse::Stay => {} + }; + if let MenuResponse::Close = self.show(response, add_contents) { + self.context_menu = None } } } diff --git a/egui/src/response.rs b/egui/src/response.rs index 0a345f6e8bac..c13cfa0dab92 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -470,6 +470,11 @@ impl Response { self.ctx.output().events.push(event); } } + + pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui, &mut super::context_menu::MenuState)) -> &Self { + self.ctx.show_ui_context_menu(&self, add_contents); + self + } } impl Response { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 043fb7589114..d16622d356eb 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -344,9 +344,6 @@ impl Ui { self.painter.set_clip_rect(clip_rect); } - pub fn context_menu(&mut self, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { - self.ctx().clone().show_ui_context_menu(self, add_contents) - } } // ------------------------------------------------------------------------ diff --git a/egui_demo_lib/src/apps/demo/drag_and_drop.rs b/egui_demo_lib/src/apps/demo/drag_and_drop.rs index 5b4679c1d23d..531c7dce9e63 100644 --- a/egui_demo_lib/src/apps/demo/drag_and_drop.rs +++ b/egui_demo_lib/src/apps/demo/drag_and_drop.rs @@ -119,12 +119,6 @@ impl super::View for DragAndDropDemo { let mut source_col_row = None; let mut drop_col = None; ui.columns(self.columns.len(), |uis| { - uis[0].context_menu(|ui, menu_state| { - if ui.button("New Item...").clicked() { - self.columns[0].push("New Item".to_string()); - menu_state.close(); - } - }); for (col_idx, column) in self.columns.clone().into_iter().enumerate() { let ui = &mut uis[col_idx]; let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯ @@ -133,7 +127,13 @@ impl super::View for DragAndDropDemo { for (row_idx, item) in column.iter().enumerate() { let item_id = Id::new("item").with(col_idx).with(row_idx); drag_source(ui, item_id, |ui| { - ui.label(item); + ui.add(Label::new(item).sense(Sense::click())) + .context_menu(|ui, menu_state| { + if ui.button("Remove...").clicked() { + self.columns[col_idx].remove(row_idx); + menu_state.close(); + } + }); }); if ui.memory().is_being_dragged(item_id) { @@ -143,6 +143,15 @@ impl super::View for DragAndDropDemo { }) .response; + if col_idx == 0 { + response.context_menu(|ui, menu_state| { + if ui.button("New Item...").clicked() { + self.columns[0].push("New Item".to_string()); + menu_state.close(); + } + }); + } + let is_being_dragged = ui.memory().is_anything_being_dragged(); if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() { drop_col = Some(col_idx); diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index df4d47ec800d..dc609535783c 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -222,8 +222,8 @@ impl WidgetGallery { ui.end_row(); ui.add(doc_link_label("Plot", "plot")); - ui.scope(|ui| { - ui.context_menu(|ui, menu_state| { + ui.add(example_plot(plot)) + .context_menu(|ui, menu_state| { if ui.button("Sin").clicked() { *plot = Plot::Sin; menu_state.close(); @@ -235,8 +235,6 @@ impl WidgetGallery { menu_state.close(); } }); - ui.add(example_plot(plot)); - }); ui.end_row(); diff --git a/egui_demo_lib/src/apps/demo/window_options.rs b/egui_demo_lib/src/apps/demo/window_options.rs index cd09a83c8ada..5d6d28067346 100644 --- a/egui_demo_lib/src/apps/demo/window_options.rs +++ b/egui_demo_lib/src/apps/demo/window_options.rs @@ -90,12 +90,13 @@ impl super::View for WindowOptions { ui.horizontal(|ui| { ui.label("title:"); ui.text_edit_singleline(title); - ui.context_menu(|ui, menu_state| { - if ui.button("Clear..").clicked() { - *title = String::new(); - menu_state.close(); - } - }); + }) + .response + .context_menu(|ui, menu_state| { + if ui.button("Clear..").clicked() { + *title = String::new(); + menu_state.close(); + } }); ui.horizontal(|ui| { diff --git a/egui_demo_lib/src/backend_panel.rs b/egui_demo_lib/src/backend_panel.rs index 59897a088fb7..73e33e71ba8a 100644 --- a/egui_demo_lib/src/backend_panel.rs +++ b/egui_demo_lib/src/backend_panel.rs @@ -92,40 +92,37 @@ impl BackendPanel { self.egui_windows.windows(ctx); } - fn context_menu(&mut self, ui: &mut egui::Ui) { + pub fn context_menu(ui: &mut egui::Ui, menu_state: &mut egui::context_menu::MenuState) { use egui::context_menu::SubMenu; - ui.context_menu(|ui, menu_state| { - if ui.button("Open...").clicked() { - menu_state.close(); - } - SubMenu::new("SubMenu") - .show(ui, menu_state, |ui, menu_state| { - if ui.button("Open...").clicked() { - menu_state.close(); - } - SubMenu::new("SubMenu") - .show(ui, menu_state, |ui, menu_state| { - if ui.button("Open...").clicked() { - menu_state.close(); - } - let _ = ui.button("Item"); - }); - let _ = ui.button("Item"); - }); - SubMenu::new("SubMenu") - .show(ui, menu_state, |ui, _menu_state| { - let _ = ui.button("Item1"); - let _ = ui.button("Item2"); - let _ = ui.button("Item3"); - let _ = ui.button("Item4"); - }); - let _ = ui.button("Item"); - }); + if ui.button("Open...").clicked() { + menu_state.close(); + } + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, menu_state| { + if ui.button("Open...").clicked() { + menu_state.close(); + } + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, menu_state| { + if ui.button("Open...").clicked() { + menu_state.close(); + } + let _ = ui.button("Item"); + }); + let _ = ui.button("Item"); + }); + SubMenu::new("SubMenu") + .show(ui, menu_state, |ui, _menu_state| { + let _ = ui.button("Item1"); + let _ = ui.button("Item2"); + let _ = ui.button("Item3"); + let _ = ui.button("Item4"); + }); + let _ = ui.button("Item"); } pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { egui::trace!(ui); - self.context_menu(ui); ui.vertical_centered(|ui| { ui.heading("💻 Backend"); }); diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 3f2e7f8484b1..f74efc443ef8 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -1,3 +1,4 @@ +use super::backend_panel::BackendPanel; /// All the different demo apps. #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -112,7 +113,9 @@ impl epi::App for WrapApp { *ui.ctx().memory() = Default::default(); } }); - }); + }) + .response + .context_menu(|ui, menu_state| BackendPanel::context_menu(ui, menu_state)); } for (anchor, app) in self.apps.iter_mut() { @@ -197,6 +200,12 @@ impl WrapApp { egui::warn_if_debug_build(ui); }); + }) + .response + .context_menu(|ui, _menu_state| { + if ui.button("Print something").clicked() { + println!("something"); + } }); } From 351950875a25a61473ca43a6cc57e25b9fad5094 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 18:41:35 +0200 Subject: [PATCH 09/46] Rename context_menu functions --- egui/src/context.rs | 5 ++--- egui/src/context_menu.rs | 2 +- egui/src/response.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index 268d7358bebf..945056a6eab8 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -305,9 +305,8 @@ impl CtxRef { Self::layer_painter(self, LayerId::debug()) } - - pub(crate) fn show_ui_context_menu(&self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { - self.context_menu_system().ui_context_menu(response, add_contents) + pub(crate) fn show_context_menu(&self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + self.context_menu_system().context_menu(response, add_contents) } } diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 8e5d475cc562..b77e4ca4373b 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -56,7 +56,7 @@ impl ContextMenuSystem { } MenuResponse::Stay } - pub fn ui_context_menu(&mut self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { + pub fn context_menu(&mut self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { match self.sense_click(response) { MenuResponse::Create(pos) => { self.context_menu = Some(ContextMenuRoot::new(pos, response.id)); diff --git a/egui/src/response.rs b/egui/src/response.rs index c13cfa0dab92..94bff49c1a45 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -472,7 +472,7 @@ impl Response { } pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui, &mut super::context_menu::MenuState)) -> &Self { - self.ctx.show_ui_context_menu(&self, add_contents); + self.ctx.show_context_menu(&self, add_contents); self } } From aa91812c29cbd47b9a5522123a48d4c729aa977e Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 19:27:12 +0200 Subject: [PATCH 10/46] Refactor --- egui/src/context_menu.rs | 2 -- egui/src/ui.rs | 1 - egui_demo_lib/src/wrap_app.rs | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index b77e4ca4373b..a1540de24595 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -209,8 +209,6 @@ impl SubMenu { } #[derive(Clone)] -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "persistence", serde(default))] pub struct MenuState { sub_menu: Option<(Id, Box)>, pub rect: Rect, diff --git a/egui/src/ui.rs b/egui/src/ui.rs index d16622d356eb..8e1828d1a584 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -4,7 +4,6 @@ use std::hash::Hash; use crate::{ color::*, containers::*, epaint::text::Fonts, layout::*, mutex::MutexGuard, placer::Placer, - context_menu::MenuState, widgets::*, *, }; diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index f74efc443ef8..7b4e90f8c774 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -115,7 +115,7 @@ impl epi::App for WrapApp { }); }) .response - .context_menu(|ui, menu_state| BackendPanel::context_menu(ui, menu_state)); + .context_menu(BackendPanel::context_menu); } for (anchor, app) in self.apps.iter_mut() { From 1da809712fbb60d3368949e7be742600e4bfd230 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Wed, 7 Jul 2021 20:40:44 +0200 Subject: [PATCH 11/46] Fix warnings --- egui/src/context_menu.rs | 1 + egui/src/response.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index a1540de24595..49dc73cfe681 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -172,6 +172,7 @@ pub struct SubMenu { text: String, } impl SubMenu { + #[allow(clippy::needless_pass_by_value)] pub fn new(text: impl ToString) -> Self { Self { text: text.to_string(), diff --git a/egui/src/response.rs b/egui/src/response.rs index 94bff49c1a45..a26ce83b0d05 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -472,7 +472,7 @@ impl Response { } pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui, &mut super::context_menu::MenuState)) -> &Self { - self.ctx.show_context_menu(&self, add_contents); + self.ctx.show_context_menu(self, add_contents); self } } From 171a81870f37d7cedd914c4bc17fb601c31fc6ac Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 13:45:52 +0200 Subject: [PATCH 12/46] Use response.clicked to detect clicks --- egui/src/context_menu.rs | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 49dc73cfe681..b491f0d26b5b 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -5,6 +5,7 @@ use super::{ Pos2, Order, Align, Layout, PointerButton, + Sense, }; #[derive(Default, Clone)] @@ -13,29 +14,20 @@ pub struct ContextMenuSystem { } impl ContextMenuSystem { fn sense_click(&mut self, response: &Response) -> MenuResponse { - let Response { - id, - ctx, - .. - } = response; - let pointer = &ctx.input().pointer; - if pointer.any_pressed() { + let response = response.interact(Sense::click_and_drag()); + let pointer = &response.ctx.input().pointer; + if pointer.any_click() { if let Some(pos) = pointer.interact_pos() { let mut destroy = false; let mut in_old_menu = false; if let Some(context_menu) = &mut self.context_menu { in_old_menu = context_menu.area_contains(pos); - destroy = context_menu.ui_id == *id; + destroy = context_menu.ui_id == response.id; } if !in_old_menu { - if response.hovered() { - if pointer.button_down(PointerButton::Secondary) { - // todo: adapt to context - return MenuResponse::Create(pos); - } else { - return MenuResponse::Close; - } - } else if destroy { + if response.secondary_clicked() { + return MenuResponse::Create(pos); + } else if response.clicked() || destroy { return MenuResponse::Close; } } From a3f0c5eb4fc8698bf88840bcd7498f7bcec9cd7e Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 13:46:16 +0200 Subject: [PATCH 13/46] Make MenuState::get_submenu private --- egui/src/context_menu.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index b491f0d26b5b..378c67db84fa 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -4,7 +4,6 @@ use super::{ Response, CtxRef, Pos2, Order, Align, Layout, - PointerButton, Sense, }; @@ -64,7 +63,6 @@ impl ContextMenuSystem { } } #[derive(Clone, PartialEq)] -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] enum MenuResponse { Close, Stay, @@ -232,7 +230,7 @@ impl MenuState { self.response = response; } } - pub fn get_submenu(&mut self, id: Id) -> Option<&mut MenuState> { + fn get_submenu(&mut self, id: Id) -> Option<&mut MenuState> { self.sub_menu.as_mut().and_then(|(k, sub)| if id == *k { Some(sub.as_mut()) } else { From 3f49d85c89fa6d49d2a281779f38e3fb924a2ece Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 14:43:57 +0200 Subject: [PATCH 14/46] Improve submenu navigation by allowing crossing other items --- egui/src/context_menu.rs | 51 +++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 378c67db84fa..7e2d29b32ea9 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -4,7 +4,8 @@ use super::{ Response, CtxRef, Pos2, Order, Align, Layout, - Sense, + Sense, Vec2, + PointerState, }; #[derive(Default, Clone)] @@ -170,23 +171,17 @@ impl SubMenu { } pub fn show(self, ui: &mut Ui, parent_state: &mut MenuState, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { let button = ui.button(self.text); - let mut sub_hovered = false; - if let Some(sub_menu) = parent_state.get_submenu(button.id) { - if let Some(pos) = ui.input().pointer.hover_pos() { - sub_hovered = sub_menu.area_contains(pos); - } - } - if !sub_hovered { + let pointer = &ui.input().pointer; + if !parent_state.hovering_current_submenu(pointer) && !parent_state.moving_towards_current_submenu(pointer) { if button.hovered() { parent_state.open_submenu(button.id); } else { - parent_state.close_submenu(button.id); + parent_state.close_submenu(); } } let responses = parent_state.get_submenu(button.id).map(|menu_state| { let response = ContextMenu::sub_menu(button.rect.right_top(), menu_state.clone()) .show(ui.ctx(), |ui| add_contents(ui, menu_state)); - // set submenu bounding box menu_state.rect = response.rect; (menu_state.response.clone(), response) }); @@ -222,6 +217,28 @@ impl MenuState { .map(|(_, sub)| sub.area_contains(pos)) .unwrap_or(false) } + fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool { + let vel_a = dir.angle(); + let top_a = (rect.left_top() - pos).angle(); + let bottom_a = (rect.left_bottom() - pos).angle(); + bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0 + } + pub(crate) fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool{ + if let Some(menu_state) = self.get_current_submenu() { + if let Some(pos) = pointer.hover_pos() { + return Self::points_at_left_of_rect(pos, pointer.velocity(), menu_state.rect); + } + } + false + } + pub(crate) fn hovering_current_submenu(&self, pointer: &PointerState) -> bool{ + if let Some(sub_menu) = self.get_current_submenu() { + if let Some(pos) = pointer.hover_pos() { + return sub_menu.area_contains(pos); + } + } + false + } pub fn close(&mut self) { self.response = MenuResponse::Close; } @@ -230,6 +247,12 @@ impl MenuState { self.response = response; } } + fn get_current_submenu_mut(&mut self) -> Option<&mut MenuState> { + self.sub_menu.as_mut().map(|(_, sub)| sub.as_mut()) + } + fn get_current_submenu(&self) -> Option<&MenuState> { + self.sub_menu.as_ref().map(|(_, sub)| sub.as_ref()) + } fn get_submenu(&mut self, id: Id) -> Option<&mut MenuState> { self.sub_menu.as_mut().and_then(|(k, sub)| if id == *k { Some(sub.as_mut()) @@ -245,12 +268,8 @@ impl MenuState { } self.sub_menu = Some((id, Box::new(MenuState::default()))); } - fn close_submenu(&mut self, id: Id) { - if let Some((k, _)) = self.sub_menu { - if k == id { - self.sub_menu = None; - } - } + fn close_submenu(&mut self) { + self.sub_menu = None; } pub fn toggle_submenu(&mut self, id: Id) { if let Some((k, _)) = self.sub_menu.take() { From 2410c7611ef0ca56e3d01151469c14e5fdc1bd02 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 15:06:14 +0200 Subject: [PATCH 15/46] Refactor and comment --- egui/src/context_menu.rs | 125 ++++++++++++++------------------------- 1 file changed, 43 insertions(+), 82 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 7e2d29b32ea9..d786952f0c33 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -13,6 +13,7 @@ pub struct ContextMenuSystem { context_menu: Option, } impl ContextMenuSystem { + /// sense if a context menu needs to be (re-)created or destroyed fn sense_click(&mut self, response: &Response) -> MenuResponse { let response = response.interact(Sense::click_and_drag()); let pointer = &response.ctx.input().pointer; @@ -35,6 +36,7 @@ impl ContextMenuSystem { } MenuResponse::Stay } + /// show the current context menu fn show(&mut self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> MenuResponse { if let Some(context_menu) = &mut self.context_menu { if context_menu.ui_id == response.id { @@ -48,6 +50,7 @@ impl ContextMenuSystem { } MenuResponse::Stay } + /// should be called from Context on a Response pub fn context_menu(&mut self, response: &Response, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) { match self.sense_click(response) { MenuResponse::Create(pos) => { @@ -74,88 +77,29 @@ impl MenuResponse { *self == Self::Close } } +/// Context menu root associated with an Id from a Response #[derive(Clone)] struct ContextMenuRoot { - context_menu: ContextMenu, + context_menu: MenuState, ui_id: Id, } impl ContextMenuRoot { pub fn new(position: Pos2, ui_id: Id) -> Self { Self { - context_menu: ContextMenu::root(position), + context_menu: MenuState::new(position), ui_id, } } - pub(crate) fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { - self.context_menu.show_root(ctx, add_contents) - } } impl std::ops::Deref for ContextMenuRoot { type Target = MenuState; fn deref(&self) -> &Self::Target { - &self.context_menu.state + &self.context_menu } } impl std::ops::DerefMut for ContextMenuRoot { fn deref_mut(&mut self) -> &mut ::Target { - &mut self.context_menu.state - } -} -#[derive(Default, Clone)] -struct ContextMenu { - state: MenuState, - position: Pos2, -} -impl ContextMenu { - pub fn root(position: Pos2) -> Self { - Self { - state: MenuState::default(), - position, - } - } - pub fn sub_menu(position: Pos2, state: MenuState) -> Self { - Self { - state, - position, - } - } - fn show_impl(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) -> Response { - Area::new(format!("context_menu_{:#?}", self.position)) - .order(Order::Foreground) - .fixed_pos(self.position) - .interactable(true) - .show(ctx, |ui| { - Frame::none() - .fill(Color32::BLACK) - .corner_radius(3.0) - .margin((0.0, 3.0)) - .show(ui, |ui| - ui.with_layout( - Layout::top_down_justified(Align::LEFT), - add_contents, - ) - ); - }) - } - pub(crate) fn show_root(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { - let mut state = self.state.clone(); - let response = self.show_impl(ctx, |ui| add_contents(ui, &mut state)); - self.state = state; - response - } - pub fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) -> Response { - self.show_impl(ctx, add_contents) - } -} -impl std::ops::Deref for ContextMenu { - type Target = MenuState; - fn deref(&self) -> &Self::Target { - &self.state - } -} -impl std::ops::DerefMut for ContextMenu { - fn deref_mut(&mut self) -> &mut ::Target { - &mut self.state + &mut self.context_menu } } #[derive(Clone)] @@ -174,14 +118,13 @@ impl SubMenu { let pointer = &ui.input().pointer; if !parent_state.hovering_current_submenu(pointer) && !parent_state.moving_towards_current_submenu(pointer) { if button.hovered() { - parent_state.open_submenu(button.id); + parent_state.open_submenu(button.id, button.rect.right_top()); } else { parent_state.close_submenu(); } } let responses = parent_state.get_submenu(button.id).map(|menu_state| { - let response = ContextMenu::sub_menu(button.rect.right_top(), menu_state.clone()) - .show(ui.ctx(), |ui| add_contents(ui, menu_state)); + let response = menu_state.show(ui.ctx(), add_contents); menu_state.rect = response.rect; (menu_state.response.clone(), response) }); @@ -209,20 +152,46 @@ impl Default for MenuState { } } } -#[allow(unused)] impl MenuState { + pub fn new(position: Pos2) -> Self { + Self { + rect: Rect::from_min_size(position, Vec2::ZERO), + ..Default::default() + } + } + pub(crate) fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { + Area::new(format!("context_menu_{:#?}", self.rect.min)) + .order(Order::Foreground) + .fixed_pos(self.rect.min) + .interactable(true) + .show(ctx, |ui| { + Frame::none() + .fill(Color32::BLACK) + .corner_radius(3.0) + .margin((0.0, 3.0)) + .show(ui, |ui| + ui.with_layout( + Layout::top_down_justified(Align::LEFT), + |ui| add_contents(ui, self), + ) + ); + }) + } + /// check if position is in the menu hierarchy's area pub(crate) fn area_contains(&self, pos: Pos2) -> bool{ self.rect.contains(pos) || self.sub_menu.as_ref() .map(|(_, sub)| sub.area_contains(pos)) .unwrap_or(false) } + /// check if dir points from pos towards left side of rect fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool { let vel_a = dir.angle(); let top_a = (rect.left_top() - pos).angle(); let bottom_a = (rect.left_bottom() - pos).angle(); bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0 } + /// check if pointer is moving towards current submenu pub(crate) fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool{ if let Some(menu_state) = self.get_current_submenu() { if let Some(pos) = pointer.hover_pos() { @@ -231,6 +200,7 @@ impl MenuState { } false } + /// check if pointer is hovering current submenu pub(crate) fn hovering_current_submenu(&self, pointer: &PointerState) -> bool{ if let Some(sub_menu) = self.get_current_submenu() { if let Some(pos) = pointer.hover_pos() { @@ -239,17 +209,16 @@ impl MenuState { } false } + /// set close response pub fn close(&mut self) { self.response = MenuResponse::Close; } + /// cascade close response to menu root fn cascade_response(&mut self, response: MenuResponse) { if response.is_close() { self.response = response; } } - fn get_current_submenu_mut(&mut self) -> Option<&mut MenuState> { - self.sub_menu.as_mut().map(|(_, sub)| sub.as_mut()) - } fn get_current_submenu(&self) -> Option<&MenuState> { self.sub_menu.as_ref().map(|(_, sub)| sub.as_ref()) } @@ -260,24 +229,16 @@ impl MenuState { None }) } - fn open_submenu(&mut self, id: Id) { + /// open submenu at position, if not already open + fn open_submenu(&mut self, id: Id, pos: Pos2) { if let Some((k, _)) = self.sub_menu { if k == id { return; } } - self.sub_menu = Some((id, Box::new(MenuState::default()))); + self.sub_menu = Some((id, Box::new(MenuState::new(pos)))); } fn close_submenu(&mut self) { self.sub_menu = None; } - pub fn toggle_submenu(&mut self, id: Id) { - if let Some((k, _)) = self.sub_menu.take() { - if k == id { - self.sub_menu = None; - return; - } - } - self.sub_menu = Some((id, Box::new(MenuState::default()))); - } } From 968233d60f1aa7de80fb2f0480134a257921950f Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 15:57:22 +0200 Subject: [PATCH 16/46] Prevent breaking of drag on attached elements --- egui/src/context_menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index d786952f0c33..579948fa02e5 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -15,7 +15,7 @@ pub struct ContextMenuSystem { impl ContextMenuSystem { /// sense if a context menu needs to be (re-)created or destroyed fn sense_click(&mut self, response: &Response) -> MenuResponse { - let response = response.interact(Sense::click_and_drag()); + let response = response.interact(Sense::click()); let pointer = &response.ctx.input().pointer; if pointer.any_click() { if let Some(pos) = pointer.interact_pos() { From c40c1204e50cf77d8a3760d2963aba3b87f584c7 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 16:15:29 +0200 Subject: [PATCH 17/46] Close submenu when pointer outside is not moving towards it we need to request an extra repaint to detect if the pointer is still even in reactive mode --- egui/src/context_menu.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 579948fa02e5..363fc52dc140 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -116,12 +116,15 @@ impl SubMenu { pub fn show(self, ui: &mut Ui, parent_state: &mut MenuState, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { let button = ui.button(self.text); let pointer = &ui.input().pointer; - if !parent_state.hovering_current_submenu(pointer) && !parent_state.moving_towards_current_submenu(pointer) { + if !parent_state.moving_towards_current_submenu(pointer) { if button.hovered() { parent_state.open_submenu(button.id, button.rect.right_top()); - } else { + } else if !parent_state.hovering_current_submenu(pointer) { parent_state.close_submenu(); } + } else { + // ensure to repaint even when pointer is not moving + ui.ctx().request_repaint(); } let responses = parent_state.get_submenu(button.id).map(|menu_state| { let response = menu_state.show(ui.ctx(), add_contents); @@ -193,9 +196,10 @@ impl MenuState { } /// check if pointer is moving towards current submenu pub(crate) fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool{ - if let Some(menu_state) = self.get_current_submenu() { + if pointer.is_still() { return false; } + if let Some(sub_menu) = self.get_current_submenu() { if let Some(pos) = pointer.hover_pos() { - return Self::points_at_left_of_rect(pos, pointer.velocity(), menu_state.rect); + return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.rect); } } false From ea27d7c475b4adb159d240a97e88b3d76eb261c5 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 16:52:55 +0200 Subject: [PATCH 18/46] Draw context menu using same Ui as egui::menu --- egui/src/context_menu.rs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 363fc52dc140..2fc5c91a9811 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -1,11 +1,9 @@ use super::{ - Color32, Id, Rect, Ui, - Frame, Area, + Id, Rect, Ui, Response, CtxRef, - Pos2, Order, - Align, Layout, - Sense, Vec2, + Pos2, Sense, Vec2, PointerState, + Style, }; #[derive(Default, Clone)] @@ -163,22 +161,14 @@ impl MenuState { } } pub(crate) fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { - Area::new(format!("context_menu_{:#?}", self.rect.min)) - .order(Order::Foreground) - .fixed_pos(self.rect.min) - .interactable(true) - .show(ctx, |ui| { - Frame::none() - .fill(Color32::BLACK) - .corner_radius(3.0) - .margin((0.0, 3.0)) - .show(ui, |ui| - ui.with_layout( - Layout::top_down_justified(Align::LEFT), - |ui| add_contents(ui, self), - ) - ); - }) + crate::menu::menu_ui( + ctx, + Id::new(format!("context_menu_{:#?}", self.rect.min)), + self.rect.min, + Style::default(), + |ui| add_contents(ui, self) + ) + .response } /// check if position is in the menu hierarchy's area pub(crate) fn area_contains(&self, pos: Pos2) -> bool{ From 0698b2b7114e206e973c132a65dd03196d84ef10 Mon Sep 17 00:00:00 2001 From: Linus Behrbohm Date: Thu, 8 Jul 2021 17:22:30 +0200 Subject: [PATCH 19/46] Improve submenu style --- egui/src/context_menu.rs | 32 +++++++++++++++++--------------- egui/src/ui.rs | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/egui/src/context_menu.rs b/egui/src/context_menu.rs index 2fc5c91a9811..02b0382c3c4e 100644 --- a/egui/src/context_menu.rs +++ b/egui/src/context_menu.rs @@ -3,7 +3,7 @@ use super::{ Response, CtxRef, Pos2, Sense, Vec2, PointerState, - Style, + Style, Button, }; #[derive(Default, Clone)] @@ -112,11 +112,18 @@ impl SubMenu { } } pub fn show(self, ui: &mut Ui, parent_state: &mut MenuState, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { - let button = ui.button(self.text); + let parent_id = ui.id(); + let sub_id = parent_id.with(format!("{:?}", ui.placer.cursor().min)); + let mut button = Button::new(format!("{} ⏵", self.text)); + if Some(sub_id) == parent_state.get_sub_id() { + button = button.fill(ui.visuals().widgets.open.bg_fill); + button = button.stroke(ui.visuals().widgets.open.bg_stroke); + } + let button = ui.add(button); let pointer = &ui.input().pointer; if !parent_state.moving_towards_current_submenu(pointer) { if button.hovered() { - parent_state.open_submenu(button.id, button.rect.right_top()); + parent_state.open_submenu(sub_id, button.rect.right_top()); } else if !parent_state.hovering_current_submenu(pointer) { parent_state.close_submenu(); } @@ -124,7 +131,7 @@ impl SubMenu { // ensure to repaint even when pointer is not moving ui.ctx().request_repaint(); } - let responses = parent_state.get_submenu(button.id).map(|menu_state| { + let responses = parent_state.get_submenu(sub_id).map(|menu_state| { let response = menu_state.show(ui.ctx(), add_contents); menu_state.rect = response.rect; (menu_state.response.clone(), response) @@ -144,26 +151,18 @@ pub struct MenuState { pub rect: Rect, response: MenuResponse, } -impl Default for MenuState { - fn default() -> Self { - Self { - rect: Rect::NOTHING, - sub_menu: None, - response: MenuResponse::Stay - } - } -} impl MenuState { pub fn new(position: Pos2) -> Self { Self { rect: Rect::from_min_size(position, Vec2::ZERO), - ..Default::default() + sub_menu: None, + response: MenuResponse::Stay } } pub(crate) fn show(&mut self, ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui, &mut MenuState)) -> Response { crate::menu::menu_ui( ctx, - Id::new(format!("context_menu_{:#?}", self.rect.min)), + Id::new(format!("{:?}", self.rect)), self.rect.min, Style::default(), |ui| add_contents(ui, self) @@ -213,6 +212,9 @@ impl MenuState { self.response = response; } } + fn get_sub_id(&self) -> Option { + self.sub_menu.as_ref().map(|(id, _)| *id) + } fn get_current_submenu(&self) -> Option<&MenuState> { self.sub_menu.as_ref().map(|(_, sub)| sub.as_ref()) } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 8e1828d1a584..60dfea439654 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -49,7 +49,7 @@ pub struct Ui { style: std::sync::Arc