Dismiss pickers with Escape before cancelling outer drafts

This commit is contained in:
2026-09-23 20:34:33 -07:00
parent 301126f609
commit e9167f5cc8
14 changed files with 1097 additions and 370 deletions
+49
View File
@@ -1,6 +1,7 @@
//! Build and show dropdown menus.
use crate::core::alignment;
use crate::core::border::{self, Border};
use crate::core::keyboard;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
@@ -15,6 +16,7 @@ use crate::core::{
};
use crate::core::{Element, Shell, Widget};
use crate::scrollable::{self, Scrollable};
use std::cell::Cell;
/// A list of selectable options.
pub struct Menu<
@@ -34,6 +36,7 @@ pub struct Menu<
hovered_option: &'a mut Option<usize>,
on_selected: Box<dyn FnMut(T) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
escape: Option<(&'a Cell<bool>, Option<Message>)>,
width: f32,
padding: Padding,
text_size: Option<Pixels>,
@@ -68,6 +71,7 @@ where
hovered_option,
on_selected: Box::new(on_selected),
on_option_hovered,
escape: None,
width: 0.0,
padding: Padding::ZERO,
text_size: None,
@@ -78,6 +82,17 @@ where
}
}
// PickList owns this flag. Other Menu consumers retain their existing
// keyboard behavior unless they explicitly opt into this private bridge.
pub(crate) fn close_on_escape(
mut self,
open: &'a Cell<bool>,
on_close: Option<Message>,
) -> Self {
self.escape = Some((open, on_close));
self
}
/// Sets the width of the [`Menu`].
pub fn width(mut self, width: f32) -> Self {
self.width = width;
@@ -170,6 +185,7 @@ where
viewport: Rectangle,
tree: &'a mut Tree,
list: Scrollable<'a, Message, Theme, Renderer>,
escape: Option<(&'a Cell<bool>, Option<Message>)>,
width: f32,
target_height: f32,
class: &'a <Theme as Catalog>::Class<'b>,
@@ -198,6 +214,7 @@ where
hovered_option,
on_selected,
on_option_hovered,
escape,
width,
padding,
font,
@@ -228,6 +245,7 @@ where
viewport,
tree: &mut state.tree,
list,
escape,
width,
target_height,
class,
@@ -278,6 +296,31 @@ where
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
// UserInterface may retain this overlay for the rest of an event batch.
// A dismissed owner must not select an option on a queued later click.
if self.escape.as_ref().is_some_and(|(open, _)| !open.get()) {
return;
}
// Consume dismissal in the overlay so Iced never sends the same key
// into focused controls or canvases beneath this picker. The shared
// owner flag also prevents duplicate close notifications in one batch.
if matches!(
event,
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
..
})
) && let Some((open, on_close)) = &mut self.escape
&& open.replace(false)
{
if let Some(message) = on_close.take() {
shell.publish(message);
}
shell.capture_event();
shell.request_redraw();
return;
}
let bounds = layout.bounds();
self.list.update(
@@ -292,6 +335,9 @@ where
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
if self.escape.as_ref().is_some_and(|(open, _)| !open.get()) {
return mouse::Interaction::None;
}
self.list.mouse_interaction(
self.tree,
layout,
@@ -309,6 +355,9 @@ where
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
if self.escape.as_ref().is_some_and(|(open, _)| !open.get()) {
return;
}
let bounds = layout.bounds();
let style = Catalog::style(theme, self.class);
+24 -10
View File
@@ -78,6 +78,7 @@ use crate::core::{
use crate::overlay::menu::{self, Menu};
use std::borrow::Borrow;
use std::cell::Cell;
use std::f32;
/// A widget for selecting a single value from a list of options.
@@ -448,12 +449,24 @@ where
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
match event {
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
..
}) if state.is_open.replace(false) => {
// The picker may have opened earlier in this base-event batch,
// after UserInterface collected its overlays.
if let Some(on_close) = &self.on_close {
shell.publish(on_close.clone());
}
shell.capture_event();
shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if state.is_open {
if state.is_open.get() {
// Event wasn't processed by overlay, so cursor was clicked either outside its
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
state.is_open.set(false);
if let Some(on_close) = &self.on_close {
shell.publish(on_close.clone());
@@ -463,7 +476,7 @@ where
} else if cursor.is_over(layout.bounds()) {
let selected = self.selected.as_ref().map(Borrow::borrow);
state.is_open = true;
state.is_open.set(true);
state.hovered_option = self
.options
.borrow()
@@ -482,7 +495,7 @@ where
}) => {
if state.keyboard_modifiers.command()
&& cursor.is_over(layout.bounds())
&& !state.is_open
&& !state.is_open.get()
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
@@ -528,7 +541,7 @@ where
let status = {
let is_hovered = cursor.is_over(layout.bounds());
if state.is_open {
if state.is_open.get() {
Status::Opened { is_hovered }
} else if is_hovered {
Status::Hovered
@@ -612,7 +625,7 @@ where
shaping,
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
Handle::Dynamic { open, closed } => {
if state.is_open {
if state.is_open.get() {
Some((
open.font,
open.code_point,
@@ -703,7 +716,7 @@ where
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
if state.is_open {
if state.is_open.get() {
let bounds = layout.bounds();
let on_select = &self.on_select;
@@ -713,13 +726,14 @@ where
self.options.borrow(),
&mut state.hovered_option,
|option| {
state.is_open = false;
state.is_open.set(false);
(on_select)(option)
},
None,
&self.menu_class,
)
.close_on_escape(&state.is_open, self.on_close.clone())
.width(bounds.width)
.padding(self.padding)
.font(font)
@@ -763,7 +777,7 @@ where
struct State<P: text::Paragraph> {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
is_open: Cell<bool>,
hovered_option: Option<usize>,
options: Vec<paragraph::Plain<P>>,
placeholder: paragraph::Plain<P>,
@@ -775,7 +789,7 @@ impl<P: text::Paragraph> State<P> {
Self {
menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
is_open: Cell::new(false),
hovered_option: Option::default(),
options: Vec::new(),
placeholder: paragraph::Plain::default(),