Files
RuDS/crates/bds-ui/src/components/keyboard.rs

1330 lines
38 KiB
Rust

#[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<Navigation> {
(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<char> {
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<Item = char>) -> 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<Item = char> {
('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<Rectangle> {
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<bool>,
direct_key: Option<char>,
code: Option<String>,
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::<State>() 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::<State>() 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>()
&& 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>() {
state.access_mode = false;
state.prefix.clear();
}
}
}
struct AccessKeyMatches<'a> {
candidate: &'a str,
count: usize,
exact: Option<String>,
}
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::<State>() 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::<State>() 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::<State>();
}
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<char>,
hint_position: HintPosition,
}
impl<'a, Message> FocusableControl<'a, Message> {
pub fn new(content: impl Into<Element<'a, Message>>, 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<Message> Widget<Message, Theme, iced::Renderer> for FocusableControl<'_, Message> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
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>();
state.enabled = self.enabled;
state.direct_key = self.hotkey;
}
fn size(&self) -> Size<Length> {
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>();
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::<State>();
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::<State>();
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::<State>();
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<overlay::Element<'b, Message, Theme, iced::Renderer>> {
self.content
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer, translation)
}
}
impl<'a, Message: 'a> From<FocusableControl<'a, Message>> for Element<'a, Message> {
fn from(control: FocusableControl<'a, Message>) -> Self {
Element::new(control)
}
}
pub fn focusable<'a, Message: 'a>(
content: impl Into<Element<'a, Message>>,
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<Element<'a, Message>>) -> 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<Message> Widget<Message, Theme, iced::Renderer> for AccessKeyScope<'_, Message> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<AccessKeyScopeState>()
}
fn state(&self) -> tree::State {
tree::State::new(AccessKeyScopeState::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
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::<AccessKeyScopeState>();
(
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::<AccessKeyScopeState>();
state.active = active;
state.prefix.clear();
} else if !are_down && were_down {
self.cancel(tree, layout, renderer);
let state = tree.state.downcast_mut::<AccessKeyScopeState>();
state.active = false;
state.prefix.clear();
state.cancelled_until_modifiers_release = false;
}
tree.state
.downcast_mut::<AccessKeyScopeState>()
.access_modifiers_down = are_down;
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
key: Key::Named(key::Named::Escape),
..
}) = &event
&& tree.state.downcast_ref::<AccessKeyScopeState>().active
{
self.cancel(tree, layout, renderer);
let state = tree.state.downcast_mut::<AccessKeyScopeState>();
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::<AccessKeyScopeState>();
(
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::<AccessKeyScopeState>();
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::<AccessKeyScopeState>().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::<AccessKeyScopeState>();
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::<AccessKeyScopeState>().active
{
self.cancel(tree, layout, renderer);
let state = tree.state.downcast_mut::<AccessKeyScopeState>();
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<overlay::Element<'b, Message, Theme, iced::Renderer>> {
self.content
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer, translation)
}
}
impl<'a, Message: 'a> From<AccessKeyScope<'a, Message>> for Element<'a, Message> {
fn from(scope: AccessKeyScope<'a, Message>) -> Self {
Element::new(scope)
}
}
pub fn scope<'a, Message: 'a>(
content: impl Into<Element<'a, Message>>,
) -> 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<Message> Widget<Message, Theme, iced::Renderer> for SuspendedKeyboard<'_, Message> {
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
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<overlay::Element<'b, Message, Theme, iced::Renderer>> {
self.content
.as_widget_mut()
.overlay(&mut tree.children[0], layout, renderer, translation)
}
}
impl<'a, Message: 'a> From<SuspendedKeyboard<'a, Message>> for Element<'a, Message> {
fn from(suspended: SuspendedKeyboard<'a, Message>) -> Self {
Element::new(suspended)
}
}
pub fn suspend<'a, Message: 'a>(
content: impl Into<Element<'a, Message>>,
) -> SuspendedKeyboard<'a, Message> {
SuspendedKeyboard {
content: content.into(),
}
}
pub struct KeyboardButton<'a, Message> {
button: Button<'a, Message>,
enabled: bool,
hotkey: Option<char>,
hint_position: HintPosition,
}
impl<'a, Message: Clone + 'a> KeyboardButton<'a, Message> {
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.button = self.button.width(width);
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.button = self.button.height(height);
self
}
pub fn padding(mut self, padding: impl Into<Padding>) -> 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<Message>) -> 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<KeyboardButton<'a, Message>> 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<Element<'a, Message>>,
) -> KeyboardButton<'a, Message> {
KeyboardButton {
button: iced_button(content),
enabled: false,
hotkey: None,
hint_position: HintPosition::Trailing,
}
}