#[cfg(test)] mod tests { use super::*; use iced::keyboard::{Key, Modifiers, key}; #[test] fn generic_codes_are_fixed_width_and_skip_direct_key_prefixes() { let reserved = letter_mask(['A', 'C']); assert_eq!(code_width(24 * 26, reserved), 2); assert_eq!(code_width(24 * 26 + 1, reserved), 3); assert_eq!(generic_code(0, 2, reserved), "BA"); assert_eq!(generic_code(25, 2, reserved), "BZ"); assert_eq!(generic_code(26, 2, reserved), "DA"); } #[test] fn tab_navigation_ignores_events_already_captured_by_an_editor() { let tab = Key::Named(key::Named::Tab); assert_eq!( tab_navigation(&tab, Modifiers::default(), iced::event::Status::Ignored), Some(Navigation::Next) ); assert_eq!( tab_navigation(&tab, Modifiers::SHIFT, iced::event::Status::Ignored), Some(Navigation::Previous) ); assert_eq!( tab_navigation(&tab, Modifiers::default(), iced::event::Status::Captured), None ); } #[test] fn access_characters_use_the_unmodified_key() { assert_eq!(access_character(&Key::Character("m".into())), Some('M')); assert_eq!(access_character(&Key::Character("1".into())), Some('1')); assert_eq!(access_character(&Key::Character("ยต".into())), None); } #[test] fn access_mode_requires_control_and_option_or_alt_together() { assert!(!access_modifiers(Modifiers::ALT)); assert!(!access_modifiers(Modifiers::CTRL)); assert!(access_modifiers(Modifiers::CTRL | Modifiers::ALT)); } #[test] fn filtered_hints_show_only_the_remaining_code() { assert_eq!(filtered_hint("AA", ""), Some("AA")); assert_eq!(filtered_hint("AA", "A"), Some("A")); assert_eq!(filtered_hint("BA", "A"), None); assert_eq!(filtered_hint("AA", "AA"), None); } #[test] fn access_inventory_excludes_disabled_and_offscreen_controls() { let mut visible = State { enabled: true, visible: Cell::new(true), ..State::default() }; let mut offscreen = State { enabled: true, visible: Cell::new(false), ..State::default() }; let mut disabled = State { enabled: false, visible: Cell::new(true), ..State::default() }; let mut inventory = AccessKeyInventory::default(); inventory.custom(&mut visible, None); inventory.custom(&mut offscreen, None); inventory.custom(&mut disabled, None); assert_eq!(inventory.control_count, 1); assert_eq!(inventory.generic_count, 1); } #[test] fn key_tip_boxes_stay_inside_their_controls() { for (bounds, length, position) in [ ( Rectangle::new((0.0, 0.0).into(), (48.0, 48.0).into()), 1, HintPosition::BottomTrailing, ), ( Rectangle::new((10.0, 20.0).into(), (60.0, 22.0).into()), 2, HintPosition::Trailing, ), ] { let hint = hint_bounds(bounds, length, position).unwrap(); assert!(hint.x >= bounds.x && hint.y >= bounds.y); assert!(hint.x + hint.width <= bounds.x + bounds.width); assert!(hint.y + hint.height <= bounds.y + bounds.height); } } #[test] fn access_prefix_filters_then_resolves_one_complete_code() { let mut first = State { code: Some("AA".into()), access_mode: true, ..State::default() }; let mut second = State { code: Some("AB".into()), access_mode: true, ..State::default() }; let mut prefix = AccessKeyMatches { candidate: "A", count: 0, exact: None, }; prefix.custom(&mut first, None); prefix.custom(&mut second, None); assert_eq!(prefix.count, 2); assert_eq!(prefix.exact, None); let mut complete = AccessKeyMatches { candidate: "AA", count: 0, exact: None, }; complete.custom(&mut first, None); complete.custom(&mut second, None); assert_eq!(complete.count, 1); assert_eq!(complete.exact.as_deref(), Some("AA")); } } use iced::advanced::layout; use iced::advanced::overlay; use iced::advanced::renderer; use iced::advanced::text::{self, Renderer as _}; use iced::advanced::widget::operation::Focusable; use iced::advanced::widget::{Id, Operation, Tree, tree}; use iced::advanced::{Clipboard, Layout, Renderer as _, Shell, Widget}; use iced::event; use iced::keyboard::{self, Key, Modifiers, key}; use iced::mouse; use iced::widget::{Button, button as iced_button}; use iced::{ Background, Border, Color, Element, Event, Length, Padding, Pixels, Rectangle, Size, Theme, Vector, alignment, }; use std::any::Any; use std::cell::Cell; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Navigation { Next, Previous, } pub fn tab_navigation( key: &Key, modifiers: Modifiers, status: event::Status, ) -> Option { (status == event::Status::Ignored && matches!(key, Key::Named(key::Named::Tab))).then(|| { if modifiers.shift() { Navigation::Previous } else { Navigation::Next } }) } fn access_character(key: &Key) -> Option { let Key::Character(value) = key else { return None; }; let mut characters = value.chars(); let key = characters.next()?; characters .next() .is_none() .then_some(key) .filter(char::is_ascii_alphanumeric) .map(|key| key.to_ascii_uppercase()) } fn access_modifiers(modifiers: Modifiers) -> bool { modifiers.alt() && modifiers.control() } fn letter_mask(keys: impl IntoIterator) -> u32 { keys.into_iter().fold(0, |mask, key| { let key = key.to_ascii_uppercase(); if key.is_ascii_uppercase() { mask | (1 << (key as u8 - b'A')) } else { mask } }) } fn available_initials(reserved: u32) -> impl Iterator { ('A'..='Z').filter(move |key| reserved & (1 << (*key as u8 - b'A')) == 0) } fn code_width(count: usize, reserved: u32) -> usize { let initials = available_initials(reserved).count().max(1); if count <= initials * 26 { 2 } else { 3 } } fn generic_code(index: usize, width: usize, reserved: u32) -> String { let suffix_capacity = 26_usize.pow((width - 1) as u32); let initial = available_initials(reserved) .nth(index / suffix_capacity) .expect("access-key capacity exceeded"); let mut code = String::with_capacity(width); code.push(initial); let suffix = index % suffix_capacity; for place in (0..width - 1).rev() { let divisor = 26_usize.pow(place as u32); code.push((b'A' + ((suffix / divisor) % 26) as u8) as char); } code } fn filtered_hint<'a>(code: &'a str, prefix: &str) -> Option<&'a str> { code.strip_prefix(prefix) .filter(|remaining| !remaining.is_empty()) } #[derive(Debug, Clone, Copy, Default)] pub enum HintPosition { #[default] Trailing, BottomTrailing, } fn hint_bounds(bounds: Rectangle, label_len: usize, position: HintPosition) -> Option { let inset = 2.0_f32; let width = (12.0 + label_len as f32 * 4.0).min((bounds.width - inset * 2.0).max(0.0)); let height = 16.0_f32.min((bounds.height - inset * 2.0).max(0.0)); if width < 12.0 || height < 12.0 { return None; } let x = bounds.x + bounds.width - width - inset; let y = match position { HintPosition::BottomTrailing => bounds.y + bounds.height - height - inset, HintPosition::Trailing => bounds.center_y() - height / 2.0, }; Some(Rectangle { x, y, width, height, }) } #[derive(Default)] struct State { focused: bool, enabled: bool, visible: Cell, direct_key: Option, code: Option, prefix: String, access_mode: bool, pending_activation: bool, } impl Focusable for State { fn is_focused(&self) -> bool { self.focused } fn focus(&mut self) { self.focused = true; } fn unfocus(&mut self) { self.focused = false; } } fn visit_children( operation: &mut dyn Operation, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(operation); } #[derive(Default)] struct AccessKeyInventory { generic_count: usize, direct_letters: u32, control_count: usize, } impl Operation for AccessKeyInventory { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { let Some(state) = state.downcast_ref::() else { return; }; if !state.enabled || !state.visible.get() { return; } self.control_count += 1; if let Some(key) = state.direct_key { self.direct_letters |= letter_mask([key]); } else { self.generic_count += 1; } } } struct AssignAccessKeys { width: usize, reserved: u32, next_generic: usize, } impl Operation for AssignAccessKeys { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { let Some(state) = state.downcast_mut::() else { return; }; state.prefix.clear(); state.access_mode = state.enabled && state.visible.get(); state.code = if !state.access_mode { None } else if let Some(key) = state.direct_key { Some(key.to_ascii_uppercase().to_string()) } else { let code = generic_code(self.next_generic, self.width, self.reserved); self.next_generic += 1; Some(code) }; } } struct SetAccessPrefix<'a> { prefix: &'a str, } impl Operation for SetAccessPrefix<'_> { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { if let Some(state) = state.downcast_mut::() && state.access_mode { state.prefix.clear(); state.prefix.push_str(self.prefix); } } } struct CancelAccessKeys; impl Operation for CancelAccessKeys { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { if let Some(state) = state.downcast_mut::() { state.access_mode = false; state.prefix.clear(); } } } struct AccessKeyMatches<'a> { candidate: &'a str, count: usize, exact: Option, } impl Operation for AccessKeyMatches<'_> { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { let Some(state) = state.downcast_ref::() else { return; }; let Some(code) = state .access_mode .then_some(state.code.as_deref()) .flatten() .filter(|code| code.starts_with(self.candidate)) else { return; }; self.count += 1; if code == self.candidate { self.exact = Some(code.to_owned()); } } } struct FocusAccessKey<'a> { target: &'a str, next_is_target: bool, } impl Operation for FocusAccessKey<'_> { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { let Some(state) = state.downcast_mut::() else { self.next_is_target = false; return; }; self.next_is_target = state.code.as_deref() == Some(self.target); state.pending_activation = self.next_is_target; } fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { if self.next_is_target { state.focus(); } else { state.unfocus(); } self.next_is_target = false; } } #[derive(Default)] struct FocusedEditable { next_is_control: bool, found: bool, } impl Operation for FocusedEditable { fn container( &mut self, _id: Option<&Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { visit_children(self, operate_on_children); } fn custom(&mut self, state: &mut dyn Any, _id: Option<&Id>) { self.next_is_control = state.is::(); } fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { self.found |= state.is_focused() && !self.next_is_control; self.next_is_control = false; } } /// Adds native focus traversal and keyboard activation to an Iced control. pub struct FocusableControl<'a, Message> { content: Element<'a, Message>, enabled: bool, hotkey: Option, hint_position: HintPosition, } impl<'a, Message> FocusableControl<'a, Message> { pub fn new(content: impl Into>, enabled: bool) -> Self { Self { content: content.into(), enabled, hotkey: None, hint_position: HintPosition::Trailing, } } pub fn hotkey(mut self, hotkey: char) -> Self { self.hotkey = Some(hotkey.to_ascii_uppercase()); self } pub fn hint_position(mut self, position: HintPosition) -> Self { self.hint_position = position; self } fn activate( &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { let cursor = mouse::Cursor::Available(layout.bounds().center()); for event in [ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), ] { let _ = self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); } } } impl Widget for FocusableControl<'_, Message> { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::default()) } fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)); let state = tree.state.downcast_mut::(); state.enabled = self.enabled; state.direct_key = self.hotkey; } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &self, tree: &mut Tree, renderer: &iced::Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget() .layout(&mut tree.children[0], renderer, limits) } fn operate( &self, tree: &mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::(); state.enabled = self.enabled; state.direct_key = self.hotkey; operation.custom(state, None); if self.enabled { operation.focusable(state, None); } else { state.unfocus(); } operation.container(None, layout.bounds(), &mut |operation| { self.content .as_widget() .operate(&mut tree.children[0], layout, renderer, operation); }); } fn on_event( &mut self, tree: &mut Tree, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &iced::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { let pending_activation = { let state = tree.state.downcast_mut::(); let pending = state.pending_activation; state.pending_activation = false; pending }; if pending_activation && self.enabled { self.activate(tree, layout, renderer, clipboard, shell, viewport); return event::Status::Captured; } let child_status = self.content.as_widget_mut().on_event( &mut tree.children[0], event.clone(), layout, cursor, renderer, clipboard, shell, viewport, ); let state = tree.state.downcast_mut::(); if child_status == event::Status::Captured || !self.enabled { return child_status; } let activate = state.focused && matches!( &event, Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(key::Named::Enter) | Key::Named(key::Named::Space), .. }) ); if activate { self.activate(tree, layout, renderer, clipboard, shell, viewport); event::Status::Captured } else { child_status } } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &iced::Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &Tree, renderer: &mut iced::Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); state.visible.set( bounds .intersection(viewport) .is_some_and(|visible| visible.width > 0.0 && visible.height > 0.0), ); if state.focused { renderer.fill_quad( renderer::Quad { bounds, border: Border { color: Color::from_rgb8(0x00, 0x7F, 0xD4), width: 2.0, radius: 6.0.into(), }, ..renderer::Quad::default() }, Background::Color(Color::TRANSPARENT), ); } if let Some(label) = state .access_mode .then_some(state.code.as_deref()) .flatten() .and_then(|code| filtered_hint(code, &state.prefix)) { let Some(badge) = hint_bounds(bounds, label.len(), self.hint_position) else { return; }; renderer.fill_quad( renderer::Quad { bounds: badge, border: Border { color: Color::BLACK, width: 1.0, radius: 4.0.into(), }, ..renderer::Quad::default() }, Background::Color(Color::from_rgb8(0xF2, 0xC1, 0x4E)), ); renderer.fill_text( text::Text { content: label.to_owned(), font: renderer.default_font(), size: Pixels(9.0), line_height: text::LineHeight::Relative(1.0), bounds: badge.size(), horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, }, badge.center(), Color::BLACK, *viewport, ); } } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, translation: Vector, ) -> Option> { self.content .as_widget_mut() .overlay(&mut tree.children[0], layout, renderer, translation) } } impl<'a, Message: 'a> From> for Element<'a, Message> { fn from(control: FocusableControl<'a, Message>) -> Self { Element::new(control) } } pub fn focusable<'a, Message: 'a>( content: impl Into>, enabled: bool, ) -> FocusableControl<'a, Message> { FocusableControl::new(content, enabled) } #[derive(Default)] struct AccessKeyScopeState { active: bool, access_modifiers_down: bool, cancelled_until_modifiers_release: bool, prefix: String, } /// Owns one access-key namespace for the visible application surface. pub struct AccessKeyScope<'a, Message> { content: Element<'a, Message>, } impl<'a, Message> AccessKeyScope<'a, Message> { fn new(content: impl Into>) -> Self { Self { content: content.into(), } } fn operate( &self, tree: &mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, operation: &mut dyn Operation, ) { self.content .as_widget() .operate(&mut tree.children[0], layout, renderer, operation); } fn begin(&self, tree: &mut Tree, layout: Layout<'_>, renderer: &iced::Renderer) -> bool { let mut focused_editable = FocusedEditable::default(); self.operate(tree, layout, renderer, &mut focused_editable); if focused_editable.found { return false; } let mut inventory = AccessKeyInventory::default(); self.operate(tree, layout, renderer, &mut inventory); if inventory.control_count == 0 { return false; } let mut assign = AssignAccessKeys { width: code_width(inventory.generic_count, inventory.direct_letters), reserved: inventory.direct_letters, next_generic: 0, }; self.operate(tree, layout, renderer, &mut assign); true } fn cancel(&self, tree: &mut Tree, layout: Layout<'_>, renderer: &iced::Renderer) { self.operate(tree, layout, renderer, &mut CancelAccessKeys); } } impl Widget for AccessKeyScope<'_, Message> { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(AccessKeyScopeState::default()) } fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &self, tree: &mut Tree, renderer: &iced::Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget() .layout(&mut tree.children[0], renderer, limits) } fn operate( &self, tree: &mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content .as_widget() .operate(&mut tree.children[0], layout, renderer, operation); }); } fn on_event( &mut self, tree: &mut Tree, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &iced::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { let modifier_update = match &event { Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) | Event::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) | Event::Keyboard(keyboard::Event::KeyReleased { modifiers, .. }) => Some(*modifiers), _ => None, }; if let Some(modifiers) = modifier_update { let (were_down, cancelled) = { let state = tree.state.downcast_ref::(); ( state.access_modifiers_down, state.cancelled_until_modifiers_release, ) }; let are_down = access_modifiers(modifiers); if are_down && !were_down && !cancelled { let active = self.begin(tree, layout, renderer); let state = tree.state.downcast_mut::(); state.active = active; state.prefix.clear(); } else if !are_down && were_down { self.cancel(tree, layout, renderer); let state = tree.state.downcast_mut::(); state.active = false; state.prefix.clear(); state.cancelled_until_modifiers_release = false; } tree.state .downcast_mut::() .access_modifiers_down = are_down; } if let Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(key::Named::Escape), .. }) = &event && tree.state.downcast_ref::().active { self.cancel(tree, layout, renderer); let state = tree.state.downcast_mut::(); state.active = false; state.prefix.clear(); state.cancelled_until_modifiers_release = true; return event::Status::Captured; } if let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = &event && access_modifiers(*modifiers) && let Some(key) = access_character(key) { let (mut active, cancelled, prefix) = { let state = tree.state.downcast_ref::(); ( state.active, state.cancelled_until_modifiers_release, state.prefix.clone(), ) }; if cancelled { return event::Status::Captured; } if !active { active = self.begin(tree, layout, renderer); if !active { return self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); } let state = tree.state.downcast_mut::(); state.active = true; state.prefix.clear(); } let candidate = format!("{prefix}{key}"); let mut matches = AccessKeyMatches { candidate: &candidate, count: 0, exact: None, }; self.operate(tree, layout, renderer, &mut matches); let count = matches.count; let exact = matches.exact.take(); drop(matches); if count == 0 { return event::Status::Captured; } self.operate( tree, layout, renderer, &mut SetAccessPrefix { prefix: &candidate }, ); tree.state.downcast_mut::().prefix = candidate; if let Some(target) = exact { self.operate( tree, layout, renderer, &mut FocusAccessKey { target: &target, next_is_target: false, }, ); let _ = self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); self.cancel(tree, layout, renderer); let state = tree.state.downcast_mut::(); state.active = false; state.prefix.clear(); state.cancelled_until_modifiers_release = true; } return event::Status::Captured; } if matches!(event, Event::Mouse(mouse::Event::ButtonPressed(_))) && tree.state.downcast_ref::().active { self.cancel(tree, layout, renderer); let state = tree.state.downcast_mut::(); state.active = false; state.prefix.clear(); } self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ) } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &iced::Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &Tree, renderer: &mut iced::Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, translation: Vector, ) -> Option> { self.content .as_widget_mut() .overlay(&mut tree.children[0], layout, renderer, translation) } } impl<'a, Message: 'a> From> for Element<'a, Message> { fn from(scope: AccessKeyScope<'a, Message>) -> Self { Element::new(scope) } } pub fn scope<'a, Message: 'a>( content: impl Into>, ) -> AccessKeyScope<'a, Message> { AccessKeyScope::new(content) } /// Keeps covered content out of keyboard traversal while a blocking overlay is open. pub struct SuspendedKeyboard<'a, Message> { content: Element<'a, Message>, } impl Widget for SuspendedKeyboard<'_, Message> { fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &self, tree: &mut Tree, renderer: &iced::Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget() .layout(&mut tree.children[0], renderer, limits) } fn operate( &self, _tree: &mut Tree, _layout: Layout<'_>, _renderer: &iced::Renderer, _operation: &mut dyn Operation, ) { } fn on_event( &mut self, tree: &mut Tree, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &iced::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { if matches!(event, Event::Keyboard(_)) { event::Status::Captured } else { self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ) } } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &iced::Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &Tree, renderer: &mut iced::Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &iced::Renderer, translation: Vector, ) -> Option> { self.content .as_widget_mut() .overlay(&mut tree.children[0], layout, renderer, translation) } } impl<'a, Message: 'a> From> for Element<'a, Message> { fn from(suspended: SuspendedKeyboard<'a, Message>) -> Self { Element::new(suspended) } } pub fn suspend<'a, Message: 'a>( content: impl Into>, ) -> SuspendedKeyboard<'a, Message> { SuspendedKeyboard { content: content.into(), } } pub struct KeyboardButton<'a, Message> { button: Button<'a, Message>, enabled: bool, hotkey: Option, hint_position: HintPosition, } impl<'a, Message: Clone + 'a> KeyboardButton<'a, Message> { pub fn width(mut self, width: impl Into) -> Self { self.button = self.button.width(width); self } pub fn height(mut self, height: impl Into) -> Self { self.button = self.button.height(height); self } pub fn padding(mut self, padding: impl Into) -> Self { self.button = self.button.padding(padding); self } pub fn on_press(mut self, message: Message) -> Self { self.button = self.button.on_press(message); self.enabled = true; self } pub fn on_press_with(mut self, message: impl Fn() -> Message + 'a) -> Self { self.button = self.button.on_press_with(message); self.enabled = true; self } pub fn on_press_maybe(mut self, message: Option) -> Self { self.enabled = message.is_some(); self.button = self.button.on_press_maybe(message); self } pub fn clip(mut self, clip: bool) -> Self { self.button = self.button.clip(clip); self } pub fn style( mut self, style: impl Fn(&Theme, iced::widget::button::Status) -> iced::widget::button::Style + 'a, ) -> Self { self.button = self.button.style(style); self } pub fn hotkey(mut self, hotkey: char) -> Self { self.hotkey = Some(hotkey); self } pub fn hint_position(mut self, position: HintPosition) -> Self { self.hint_position = position; self } } impl<'a, Message: Clone + 'a> From> for Element<'a, Message> { fn from(button: KeyboardButton<'a, Message>) -> Self { let control = FocusableControl::new(button.button, button.enabled) .hint_position(button.hint_position); match button.hotkey { Some(hotkey) => control.hotkey(hotkey).into(), None => control.into(), } } } pub fn button<'a, Message: Clone + 'a>( content: impl Into>, ) -> KeyboardButton<'a, Message> { KeyboardButton { button: iced_button(content), enabled: false, hotkey: None, hint_position: HintPosition::Trailing, } }