Dismiss pickers with Escape before cancelling outer drafts
This commit is contained in:
+49
@@ -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
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user