191 lines
6.5 KiB
Rust
191 lines
6.5 KiB
Rust
use super::*;
|
|
use iced::advanced::widget::operation::{Focusable, Outcome};
|
|
use iced::widget::{column, text_input};
|
|
use iced::{Point, Theme};
|
|
|
|
#[derive(Default)]
|
|
struct FocusMap(Vec<(Option<widget::Id>, Rectangle, bool)>);
|
|
impl widget::Operation for FocusMap {
|
|
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
|
visit(self);
|
|
}
|
|
fn focusable(&mut self, id: Option<&widget::Id>, bounds: Rectangle, state: &mut dyn Focusable) {
|
|
self.0.push((id.cloned(), bounds, state.is_focused()));
|
|
}
|
|
}
|
|
|
|
struct Mounted {
|
|
element: Element<'static, (), Theme, ()>,
|
|
tree: widget::Tree,
|
|
node: layout::Node,
|
|
}
|
|
impl Mounted {
|
|
fn new(clock: Arc<AtomicU64>) -> Self {
|
|
let content = column![
|
|
text_input("Editor", "source")
|
|
.id("editor")
|
|
.on_input(|_| ())
|
|
.padding(12),
|
|
text_input("Find", "")
|
|
.id("find")
|
|
.on_input(|_| ())
|
|
.padding(12),
|
|
]
|
|
.spacing(10)
|
|
.into();
|
|
let mut element = observe(content, clock);
|
|
let mut tree = widget::Tree::new(element.as_widget());
|
|
let node = element.as_widget_mut().layout(
|
|
&mut tree,
|
|
&(),
|
|
&layout::Limits::new(Size::ZERO, Size::new(400., 200.)),
|
|
);
|
|
Self {
|
|
element,
|
|
tree,
|
|
node,
|
|
}
|
|
}
|
|
fn pass(&mut self, operation: &mut dyn widget::Operation) {
|
|
self.element.as_widget_mut().operate(
|
|
&mut self.tree,
|
|
Layout::new(&self.node),
|
|
&(),
|
|
operation,
|
|
);
|
|
}
|
|
fn run(&mut self, operation: impl widget::Operation + 'static) {
|
|
let mut next: Box<dyn widget::Operation> = Box::new(operation);
|
|
loop {
|
|
self.pass(next.as_mut());
|
|
match next.finish() {
|
|
Outcome::Chain(chained) => next = chained,
|
|
Outcome::None => break,
|
|
Outcome::Some(()) => panic!("focus must not publish an application message"),
|
|
}
|
|
}
|
|
}
|
|
fn focus(&mut self) -> FocusMap {
|
|
let mut focus = FocusMap::default();
|
|
self.pass(&mut focus);
|
|
focus
|
|
}
|
|
fn click(&mut self, id: &str) {
|
|
let bounds = self
|
|
.focus()
|
|
.0
|
|
.into_iter()
|
|
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
|
.unwrap()
|
|
.1;
|
|
let mut messages = Vec::new();
|
|
let mut shell = Shell::new(&mut messages);
|
|
self.element.as_widget_mut().update(
|
|
&mut self.tree,
|
|
&Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
|
Layout::new(&self.node),
|
|
mouse::Cursor::Available(bounds.center()),
|
|
&(),
|
|
&mut iced::advanced::clipboard::Null,
|
|
&mut shell,
|
|
&Rectangle::with_size(Size::new(400., 200.)),
|
|
);
|
|
assert!(shell.is_event_captured());
|
|
assert!(
|
|
messages.is_empty(),
|
|
"focus-only click must not rely on App messages"
|
|
);
|
|
}
|
|
fn focused(&mut self, id: &str) -> bool {
|
|
self.focus()
|
|
.0
|
|
.into_iter()
|
|
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
|
.unwrap()
|
|
.2
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn captured_find_click_invalidates_without_app_message_and_preserves_focus() {
|
|
let clock = Arc::new(AtomicU64::new(0));
|
|
let mut ui = Mounted::new(clock.clone());
|
|
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
|
assert!(ui.focused("editor"));
|
|
ui.click("find");
|
|
assert!(!is_current(&clock, 0));
|
|
assert!(ui.focused("find"));
|
|
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
|
assert!(ui.focused("find"));
|
|
// Even a matching clock cannot authorize stealing another widget's focus.
|
|
ui.run(GuardedFocus::new(
|
|
"editor".into(),
|
|
clock.clone(),
|
|
clock.load(Ordering::Acquire),
|
|
));
|
|
assert!(ui.focused("find"));
|
|
assert!(!ui.focused("editor"));
|
|
}
|
|
|
|
#[test]
|
|
fn focus_rechecks_clock_between_inspection_and_application_and_missing_target_is_inert() {
|
|
let clock = Arc::new(AtomicU64::new(0));
|
|
let mut ui = Mounted::new(clock.clone());
|
|
let mut operation = GuardedFocus::new("editor".into(), clock.clone(), 0);
|
|
ui.pass(&mut operation);
|
|
let Outcome::Chain(mut apply) = <GuardedFocus as widget::Operation<()>>::finish(&operation)
|
|
else {
|
|
panic!("mounted, unopposed target must be eligible");
|
|
};
|
|
clock.store(1, Ordering::Release);
|
|
ui.pass(apply.as_mut());
|
|
assert!(!ui.focused("editor"));
|
|
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 1));
|
|
assert!(ui.focused("editor"));
|
|
ui.run(GuardedFocus::new("removed-editor".into(), clock, 1));
|
|
assert!(ui.focused("editor"));
|
|
}
|
|
|
|
#[test]
|
|
fn raw_navigation_inputs_advance_but_passive_geometry_does_not_and_exhaustion_refuses() {
|
|
let clock = AtomicU64::new(0);
|
|
let events = [
|
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
|
key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
|
modified_key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
|
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::ArrowLeft),
|
|
location: keyboard::Location::Standard,
|
|
modifiers: keyboard::Modifiers::empty(),
|
|
text: None,
|
|
repeat: false,
|
|
}),
|
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
|
Event::Mouse(mouse::Event::WheelScrolled {
|
|
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
|
|
}),
|
|
Event::Touch(touch::Event::FingerPressed {
|
|
id: touch::Finger(1),
|
|
position: Point::ORIGIN,
|
|
}),
|
|
Event::Window(iced::window::Event::Unfocused),
|
|
];
|
|
for (index, event) in events.iter().enumerate() {
|
|
invalidate(&clock, event);
|
|
assert_eq!(clock.load(Ordering::Acquire), index as u64 + 1);
|
|
}
|
|
invalidate(
|
|
&clock,
|
|
&Event::Mouse(mouse::Event::CursorMoved {
|
|
position: Point::ORIGIN,
|
|
}),
|
|
);
|
|
invalidate(&clock, &Event::Window(iced::window::Event::Focused));
|
|
assert!(is_current(&clock, 5));
|
|
clock.store(u64::MAX - 1, Ordering::Release);
|
|
invalidate(&clock, &events[0]);
|
|
assert!(!is_current(&clock, u64::MAX));
|
|
invalidate(&clock, &events[0]);
|
|
assert_eq!(clock.load(Ordering::Acquire), u64::MAX);
|
|
assert!(!is_current(&clock, 0));
|
|
}
|