1330 lines
38 KiB
Rust
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,
|
|
}
|
|
}
|