Combine source navigation and alias rename with canvas ownership guards
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user