Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context menu #543

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
01aea23
Only drag with primary pointer button
mankinskin Jul 6, 2021
04e52be
Add ContextMenuSystem for context menu
mankinskin Jul 3, 2021
432ac9c
Syntax simplification in demo context menu builder
mankinskin Jul 3, 2021
185d86e
Store ContextMenuSystem in Context and respect Ui clicked
mankinskin Jul 6, 2021
69a33d8
Add context menu for widget_gallery plot
mankinskin Jul 6, 2021
28ef15d
Fix menu being reset when clicking on it
mankinskin Jul 6, 2021
1817b62
Adapt to removal of From conversions Vec2 <-> Pos2
mankinskin Jul 6, 2021
7011c37
Apply context menu to Response
mankinskin Jul 7, 2021
3519508
Rename context_menu functions
mankinskin Jul 7, 2021
aa91812
Refactor
mankinskin Jul 7, 2021
1da8097
Fix warnings
mankinskin Jul 7, 2021
171a818
Use response.clicked to detect clicks
mankinskin Jul 8, 2021
a3f0c5e
Make MenuState::get_submenu private
mankinskin Jul 8, 2021
3f49d85
Improve submenu navigation by allowing crossing other items
mankinskin Jul 8, 2021
2410c76
Refactor and comment
mankinskin Jul 8, 2021
968233d
Prevent breaking of drag on attached elements
mankinskin Jul 8, 2021
c40c120
Close submenu when pointer outside is not moving towards it
mankinskin Jul 8, 2021
ea27d7c
Draw context menu using same Ui as egui::menu
mankinskin Jul 8, 2021
0698b2b
Improve submenu style
mankinskin Jul 8, 2021
3e0e7aa
Fix submenu button flicker
mankinskin Jul 8, 2021
f25e02f
Better encapsulation of MenuState
mankinskin Jul 8, 2021
5e96b56
Remove Clone implementations
mankinskin Jul 8, 2021
8c46041
Some styling improvements
mankinskin Jul 8, 2021
31e9fd4
Fix submenu text alignment
mankinskin Jul 9, 2021
2a1ac41
Refactor, run cargo fmt
mankinskin Jul 9, 2021
ccbd1d2
Add function for adding items to submenus
mankinskin Jul 23, 2021
dee2611
Keep track of submenu index in parent context menu
mankinskin Jul 23, 2021
1697958
Refactoring
mankinskin Jul 23, 2021
ae95f5f
Add button padding to context menu
mankinskin Jul 23, 2021
97c957a
Don't return any Response from SubMenu::show
mankinskin Jul 23, 2021
85d63b1
Define useful Response for SubMenu::show
mankinskin Jul 24, 2021
1238467
Open ContextMenu on pointer button press, not click
mankinskin Jul 25, 2021
3ed068a
Refactoring: Apply PR Review
mankinskin Aug 27, 2021
557184f
cargo clippy
mankinskin Aug 27, 2021
5a31048
Add MenuUi and demo window
mankinskin Aug 27, 2021
2f573a6
Remove MenuUi and manage MenuState in Ui
mankinskin Aug 29, 2021
b8fe011
Use vscroll() instead of scroll()
mankinskin Aug 29, 2021
aef287b
submenus for normal egui:::menus
mankinskin Aug 29, 2021
88d3900
Fix stationary menus not closing when clicking elsewhere
mankinskin Aug 29, 2021
9f3af74
Rebase on emilk/egui@master
mankinskin Sep 29, 2021
700b166
Fix stationary menu rect not being set
mankinskin Sep 29, 2021
37ac070
Apply review
mankinskin Sep 29, 2021
ffbf96f
Fix stationary menus not reacting to close_menu()
mankinskin Sep 29, 2021
a74dff7
cargo fmt & clippy
mankinskin Sep 29, 2021
b54233c
Delete context_menu module
mankinskin Sep 29, 2021
3db6bdf
Run cargo clippy and cargo fmt
mankinskin Oct 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add ContextMenuSystem for context menu
Listens for secondary click, support for sub-menus
  • Loading branch information
mankinskin committed Oct 25, 2021
commit 04e52be74e15aaac3b854ac10c8969dd7a9d9dc3
220 changes: 220 additions & 0 deletions egui/src/context_menu.rs
Original file line number Diff line number Diff line change
@@ -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<ContextMenu>,
}
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 <Self as std::ops::Deref>::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<MenuState>)>,
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())));
}
}
1 change: 1 addition & 0 deletions egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions egui_demo_lib/src/wrap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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| {
Expand Down