Live context menu (#1447)
Depends on: - [x] https://github.com/pop-os/iced/pull/400 We refresh the open popup's items. If the number and shape of the items don't change (just the labels change) then we reuse the menu, otherwise we create a new one, so if you right click on a file and some other process deletes the file, the menu blinks and refreshes with different items. But if you press shift the menu updates in place. Also I noticed another bug that was there before, if you select both files and directories in cosmic-files and right click, the menu that opens is visually corrupted. It's because the "open" and "open with" are removed, so the first item is a divider. I added a commit that removes leading dividers (we were removing trailing dividers before). ___ - [x] I have disclosed use of any AI generated code in my commit messages. - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. - [x] I understand these changes in full and will be able to respond to review comments. - [x] My change is accurately described in the commit message. - [x] My contribution is tested and working as described. - [x] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
This commit is contained in:
commit
03c8f93b29
3 changed files with 68 additions and 13 deletions
2
iced
2
iced
|
|
@ -1 +1 @@
|
||||||
Subproject commit 5e2b580791e994cb0132c94b32ba4b00bc9ced21
|
Subproject commit 6f1b3a71704c4074fad03b3bd6a4203f69556c9e
|
||||||
|
|
@ -17,6 +17,8 @@ use iced_core::{Length, Point, Size, mouse, touch};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::widget::RcWrapper;
|
||||||
|
|
||||||
/// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation.
|
/// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation.
|
||||||
pub fn context_menu<'a, Message: 'static + Clone>(
|
pub fn context_menu<'a, Message: 'static + Clone>(
|
||||||
content: impl Into<crate::Element<'a, Message>>,
|
content: impl Into<crate::Element<'a, Message>>,
|
||||||
|
|
@ -71,7 +73,11 @@ pub struct ContextMenu<'a, Message> {
|
||||||
|
|
||||||
impl<Message: Clone + 'static> ContextMenu<'_, Message> {
|
impl<Message: Clone + 'static> ContextMenu<'_, Message> {
|
||||||
/// Publish `on_open`/`on_close` when the open state changed since the last report.
|
/// Publish `on_open`/`on_close` when the open state changed since the last report.
|
||||||
fn report_open_state(&self, state: &mut LocalState, shell: &mut iced_core::Shell<'_, Message>) {
|
fn report_open_state(
|
||||||
|
&self,
|
||||||
|
state: &mut LocalState<Message>,
|
||||||
|
shell: &mut iced_core::Shell<'_, Message>,
|
||||||
|
) {
|
||||||
let open = state.menu_bar_state.inner.with_data(|d| d.open);
|
let open = state.menu_bar_state.inner.with_data(|d| d.open);
|
||||||
if open == state.reported_open {
|
if open == state.reported_open {
|
||||||
return;
|
return;
|
||||||
|
|
@ -92,7 +98,7 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
|
||||||
renderer: &crate::Renderer,
|
renderer: &crate::Renderer,
|
||||||
shell: &mut iced_core::Shell<'_, Message>,
|
shell: &mut iced_core::Shell<'_, Message>,
|
||||||
viewport: &iced::Rectangle,
|
viewport: &iced::Rectangle,
|
||||||
my_state: &mut LocalState,
|
my_state: &mut LocalState<Message>,
|
||||||
) {
|
) {
|
||||||
if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
|
if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
|
||||||
use crate::surface::action::{LiveSettings, destroy_popup};
|
use crate::surface::action::{LiveSettings, destroy_popup};
|
||||||
|
|
@ -197,6 +203,7 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let parent = self.window_id;
|
let parent = self.window_id;
|
||||||
|
let roots = my_state.roots.clone();
|
||||||
let t = THEME.lock().unwrap();
|
let t = THEME.lock().unwrap();
|
||||||
let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
|
let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
|
||||||
drop(t);
|
drop(t);
|
||||||
|
|
@ -223,8 +230,12 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
|
||||||
input_zone: None,
|
input_zone: None,
|
||||||
},
|
},
|
||||||
Some(move || {
|
Some(move || {
|
||||||
|
// Latest roots from the owner widget
|
||||||
|
let mut popup_menu = popup_menu.clone();
|
||||||
|
popup_menu.menu_roots =
|
||||||
|
std::borrow::Cow::Owned(roots.with_data(Clone::clone));
|
||||||
crate::Element::from(
|
crate::Element::from(
|
||||||
crate::widget::container(popup_menu.clone()).center(Length::Fill),
|
crate::widget::container(popup_menu).center(Length::Fill),
|
||||||
)
|
)
|
||||||
.map(crate::action::app)
|
.map(crate::action::app)
|
||||||
}),
|
}),
|
||||||
|
|
@ -246,7 +257,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
for ContextMenu<'_, Message>
|
for ContextMenu<'_, Message>
|
||||||
{
|
{
|
||||||
fn tag(&self) -> tree::Tag {
|
fn tag(&self) -> tree::Tag {
|
||||||
tree::Tag::of::<LocalState>()
|
tree::Tag::of::<LocalState<Message>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(&self) -> tree::State {
|
fn state(&self) -> tree::State {
|
||||||
|
|
@ -256,6 +267,8 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
fingers_pressed: Default::default(),
|
fingers_pressed: Default::default(),
|
||||||
menu_bar_state: Default::default(),
|
menu_bar_state: Default::default(),
|
||||||
reported_open: false,
|
reported_open: false,
|
||||||
|
roots: RcWrapper::new(self.context_menu.clone().unwrap_or_default()),
|
||||||
|
reshape: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,11 +302,31 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
|
|
||||||
fn diff(&mut self, tree: &mut Tree) {
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
tree.diff_children(std::slice::from_mut(&mut self.content));
|
tree.diff_children(std::slice::from_mut(&mut self.content));
|
||||||
let state = tree.state.downcast_mut::<LocalState>();
|
let state = tree.state.downcast_mut::<LocalState<Message>>();
|
||||||
if let Some(context_menu) = self.context_menu.as_mut() {
|
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||||
|
// The popup's item slots were measured at open from the item widgets. Items of
|
||||||
|
// another kind or count, such as a divider in a new place, do not fit those slots,
|
||||||
|
// so rebuild the popup on the next `update`. Same-shaped items, such as a
|
||||||
|
// relabeled button, update in place.
|
||||||
|
let popup_open = state
|
||||||
|
.menu_bar_state
|
||||||
|
.inner
|
||||||
|
.with_data(|d| !d.popup_id.is_empty());
|
||||||
|
let shape = |roots: &Vec<menu::Tree<Message>>| -> Vec<tree::Tag> {
|
||||||
|
roots.first().map_or_else(Vec::new, |root| {
|
||||||
|
root.flattern().iter().map(|mt| mt.item.tag()).collect()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if popup_open && state.roots.with_data(shape) != shape(context_menu) {
|
||||||
|
state.reshape = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.menu_bar_state.inner.with_data_mut(|inner| {
|
state.menu_bar_state.inner.with_data_mut(|inner| {
|
||||||
menu_roots_diff(context_menu, &mut inner.tree);
|
menu_roots_diff(context_menu, &mut inner.tree);
|
||||||
});
|
});
|
||||||
|
state
|
||||||
|
.roots
|
||||||
|
.with_data_mut(|roots| roots.clone_from(context_menu));
|
||||||
}
|
}
|
||||||
|
|
||||||
// if let Some(ref mut context_menus) = self.context_menu {
|
// if let Some(ref mut context_menus) = self.context_menu {
|
||||||
|
|
@ -398,7 +431,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
shell: &mut iced_core::Shell<'_, Message>,
|
shell: &mut iced_core::Shell<'_, Message>,
|
||||||
viewport: &iced::Rectangle,
|
viewport: &iced::Rectangle,
|
||||||
) {
|
) {
|
||||||
let state = tree.state.downcast_mut::<LocalState>();
|
let state = tree.state.downcast_mut::<LocalState<Message>>();
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
// The compositor dismissed our popup: nothing else tells this state about it.
|
// The compositor dismissed our popup: nothing else tells this state about it.
|
||||||
|
|
@ -415,6 +448,24 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The menu changed shape while open
|
||||||
|
#[cfg(wayland_platform)]
|
||||||
|
if state.reshape {
|
||||||
|
state.reshape = false;
|
||||||
|
if state.menu_bar_state.inner.with_data(|d| d.open) {
|
||||||
|
if let Some(context_menu) = self.context_menu.as_mut() {
|
||||||
|
state.menu_bar_state.inner.with_data_mut(|inner| {
|
||||||
|
menu_roots_diff(context_menu, &mut inner.tree);
|
||||||
|
});
|
||||||
|
state
|
||||||
|
.roots
|
||||||
|
.with_data_mut(|roots| roots.clone_from(context_menu));
|
||||||
|
}
|
||||||
|
let view_cursor = state.menu_bar_state.inner.with_data(|d| d.view_cursor);
|
||||||
|
self.create_popup(layout, view_cursor, renderer, shell, viewport, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open.
|
// XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open.
|
||||||
let reset = self.window_id != window::Id::NONE
|
let reset = self.window_id != window::Id::NONE
|
||||||
&& state
|
&& state
|
||||||
|
|
@ -484,7 +535,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
&& (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2))
|
&& (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2))
|
||||||
{
|
{
|
||||||
state.context_cursor = cursor.position().unwrap_or_default();
|
state.context_cursor = cursor.position().unwrap_or_default();
|
||||||
let state = tree.state.downcast_mut::<LocalState>();
|
let state = tree.state.downcast_mut::<LocalState<Message>>();
|
||||||
state.menu_bar_state.inner.with_data_mut(|state| {
|
state.menu_bar_state.inner.with_data_mut(|state| {
|
||||||
state.open = true;
|
state.open = true;
|
||||||
state.view_cursor = cursor;
|
state.view_cursor = cursor;
|
||||||
|
|
@ -496,7 +547,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
|
|
||||||
shell.request_redraw();
|
shell.request_redraw();
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
self.report_open_state(tree.state.downcast_mut::<LocalState>(), shell);
|
self.report_open_state(tree.state.downcast_mut::<LocalState<Message>>(), shell);
|
||||||
return;
|
return;
|
||||||
} else if !was_open && right_button_released(event)
|
} else if !was_open && right_button_released(event)
|
||||||
|| (touch_lifted(event))
|
|| (touch_lifted(event))
|
||||||
|
|
@ -532,7 +583,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
shell,
|
shell,
|
||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
self.report_open_state(tree.state.downcast_mut::<LocalState>(), shell);
|
self.report_open_state(tree.state.downcast_mut::<LocalState<Message>>(), shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overlay<'b>(
|
fn overlay<'b>(
|
||||||
|
|
@ -560,7 +611,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = tree.state.downcast_ref::<LocalState>();
|
let state = tree.state.downcast_ref::<LocalState<Message>>();
|
||||||
let Some(context_menu) = self.context_menu.as_mut() else {
|
let Some(context_menu) = self.context_menu.as_mut() else {
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
@ -641,9 +692,12 @@ fn touch_lifted(event: &Event) -> bool {
|
||||||
matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
|
matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LocalState {
|
pub struct LocalState<Message> {
|
||||||
context_cursor: Point,
|
context_cursor: Point,
|
||||||
fingers_pressed: HashSet<Finger>,
|
fingers_pressed: HashSet<Finger>,
|
||||||
menu_bar_state: MenuBarState,
|
menu_bar_state: MenuBarState,
|
||||||
reported_open: bool,
|
reported_open: bool,
|
||||||
|
roots: RcWrapper<Vec<menu::Tree<Message>>>,
|
||||||
|
/// menu shape has changed, rebuild it on the next `update`
|
||||||
|
reshape: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -445,7 +445,8 @@ pub fn menu_items<
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
MenuItem::Divider => {
|
MenuItem::Divider => {
|
||||||
if i != size - 1 {
|
// A divider at either end separates nothing
|
||||||
|
if i != 0 && i != size - 1 {
|
||||||
trees.push(MenuTree::<Message>::from(Element::from(
|
trees.push(MenuTree::<Message>::from(Element::from(
|
||||||
widget::divider::horizontal::light(),
|
widget::divider::horizontal::light(),
|
||||||
)));
|
)));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue