Reject stale navigation envelopes before shared boundaries
This commit is contained in:
@@ -65,6 +65,7 @@ impl Root<'_> {
|
|||||||
view_index: Some(self.app.document.view_index()),
|
view_index: Some(self.app.document.view_index()),
|
||||||
diagram,
|
diagram,
|
||||||
selected: self.app.selected,
|
selected: self.app.selected,
|
||||||
|
navigation_focus: None,
|
||||||
zoom: self.app.zoom,
|
zoom: self.app.zoom,
|
||||||
pan: self.app.pan,
|
pan: self.app.pan,
|
||||||
context: self.app.route_context().unwrap(),
|
context: self.app.route_context().unwrap(),
|
||||||
|
|||||||
@@ -1249,6 +1249,21 @@ impl App {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
// These handlers reject old owners/tokens without changing state. Apply
|
||||||
|
// the same admission before shared camera and allocation boundaries.
|
||||||
|
if !self.source_input_envelope(&message)
|
||||||
|
|| matches!(&message, Message::DiagramUsage(event) if !self.diagram_usage_envelope(event))
|
||||||
|
|| matches!(&message, Message::WorkDrag(event) if !self.allocation_event_envelope(event))
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if let Message::WorkDrag(allocation_drag::Event::Press(press)) = &message
|
||||||
|
&& !self.allocation_press_owns_current_view(press)
|
||||||
|
{
|
||||||
|
// Keep the existing stale-press explanation, without treating the
|
||||||
|
// rejected press as a new navigation or allocation boundary.
|
||||||
|
return self.allocation_event(allocation_drag::Event::Press(press.clone()));
|
||||||
|
}
|
||||||
// A queued toolbar from a previous input owner cannot retire a newer
|
// A queued toolbar from a previous input owner cannot retire a newer
|
||||||
// navigation destination or allocation capability before dispatch.
|
// navigation destination or allocation capability before dispatch.
|
||||||
if let Message::BehaviorControl(intent) = &message
|
if let Message::BehaviorControl(intent) = &message
|
||||||
@@ -1381,29 +1396,16 @@ impl App {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn update_inner(&mut self, message: Message) -> Task<Message> {
|
fn update_inner(&mut self, message: Message) -> Task<Message> {
|
||||||
|
if !self.source_input_envelope(&message) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let message = match message {
|
let message = match message {
|
||||||
Message::SourceFindClose(context, instance) => {
|
Message::SourceFindClose(..) => {
|
||||||
if context != self.source_symbol_context()
|
|
||||||
|| instance != self.source_find_instance
|
|
||||||
|| !self.source_navigation.open
|
|
||||||
|| !self.source_navigation_available()
|
|
||||||
{
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
// Only the checked envelope may enter the legacy Close handler.
|
// Only the checked envelope may enter the legacy Close handler.
|
||||||
Message::SourceNavigation(crate::source_navigation::Event::Close)
|
Message::SourceNavigation(crate::source_navigation::Event::Close)
|
||||||
}
|
}
|
||||||
Message::SourceNavigation(crate::source_navigation::Event::Close) => {
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
message => message,
|
message => message,
|
||||||
};
|
};
|
||||||
if let Message::SourceSymbols(context, _) = &message
|
|
||||||
&& (context.session != self.source_workspace.session()
|
|
||||||
|| context.path != self.source_workspace.active)
|
|
||||||
{
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
// Internal results and geometry do not constitute a new user intent.
|
// Internal results and geometry do not constitute a new user intent.
|
||||||
// Process query completion even behind a modal to release its worker slot;
|
// Process query completion even behind a modal to release its worker slot;
|
||||||
// its handler independently refuses navigation outside the live editor.
|
// its handler independently refuses navigation outside the live editor.
|
||||||
|
|||||||
@@ -437,7 +437,29 @@ impl App {
|
|||||||
self.refresh_draft();
|
self.refresh_draft();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
pub(super) fn allocation_press_owns_current_view(&self, press: &Press) -> bool {
|
||||||
|
self.allocation_drag.active.is_none()
|
||||||
|
&& self.allocation_stamp_valid(press.stamp)
|
||||||
|
&& press.arrange == self.allocation_drag.arrange_next
|
||||||
|
}
|
||||||
|
pub(super) fn allocation_event_envelope(&self, event: &Event) -> bool {
|
||||||
|
match event {
|
||||||
|
Event::Cancel(token) | Event::Preview(_, token, _) | Event::Release(_, token, _) => {
|
||||||
|
self.allocation_drag
|
||||||
|
.active
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|active| active.press.token == *token)
|
||||||
|
}
|
||||||
|
Event::Refused(epoch, _) => *epoch == self.allocation_drag.epoch,
|
||||||
|
// Invalid Press packets deliberately report why the view or draft
|
||||||
|
// changed. Leave their existing validation and feedback intact.
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub(super) fn allocation_event(&mut self, event: Event) -> Task<Message> {
|
pub(super) fn allocation_event(&mut self, event: Event) -> Task<Message> {
|
||||||
|
if !self.allocation_event_envelope(&event) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
match event {
|
match event {
|
||||||
Event::ArrangeNext => {
|
Event::ArrangeNext => {
|
||||||
self.allocation_drag.retire();
|
self.allocation_drag.retire();
|
||||||
@@ -475,9 +497,7 @@ impl App {
|
|||||||
if self.allocation_drag.active.is_some() {
|
if self.allocation_drag.active.is_some() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if !self.allocation_stamp_valid(press.stamp)
|
if !self.allocation_press_owns_current_view(&press) {
|
||||||
|| press.arrange != self.allocation_drag.arrange_next
|
|
||||||
{
|
|
||||||
self.status = if self.allocation_drag_clean() {
|
self.status = if self.allocation_drag_clean() {
|
||||||
"The view changed. Start this drag again.".into()
|
"The view changed. Start this drag again.".into()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use iced::{
|
|||||||
#[path = "allocation_drag_full_tree_tests.rs"]
|
#[path = "allocation_drag_full_tree_tests.rs"]
|
||||||
mod full_tree;
|
mod full_tree;
|
||||||
|
|
||||||
|
mod navigation_union;
|
||||||
|
|
||||||
struct Fixture {
|
struct Fixture {
|
||||||
app: App,
|
app: App,
|
||||||
a: Id,
|
a: Id,
|
||||||
@@ -61,6 +63,7 @@ fn canvas(app: &App) -> DiagramCanvas<'_> {
|
|||||||
view_index: Some(app.document.view_index()),
|
view_index: Some(app.document.view_index()),
|
||||||
diagram: app.active_diagram.unwrap(),
|
diagram: app.active_diagram.unwrap(),
|
||||||
selected: app.selected,
|
selected: app.selected,
|
||||||
|
navigation_focus: None,
|
||||||
zoom: app.zoom,
|
zoom: app.zoom,
|
||||||
pan: app.pan,
|
pan: app.pan,
|
||||||
context: app.route_context().unwrap(),
|
context: app.route_context().unwrap(),
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
//! Captured real canvas packets against a later navigation or live gesture.
|
||||||
|
use super::*;
|
||||||
|
use crate::diagram_usage::{self as usage, Target};
|
||||||
|
|
||||||
|
fn navigate_work(f: &mut Fixture) -> usage::Usage {
|
||||||
|
let message = f.app.diagram_usage_message(f.work);
|
||||||
|
let _ = f.app.update(message);
|
||||||
|
let panel = f.app.diagram_usages.as_ref().unwrap();
|
||||||
|
let token = panel.token;
|
||||||
|
let context = panel.context;
|
||||||
|
let process = f.app.behavior.topology.usage_snapshot().into_query_state();
|
||||||
|
let completion = usage::Completion {
|
||||||
|
token,
|
||||||
|
context,
|
||||||
|
result: Ok(usage::collect(
|
||||||
|
f.app.project(),
|
||||||
|
f.work,
|
||||||
|
&process,
|
||||||
|
&crate::behavior::scenario::State::default(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
let _ = f.app.update(Message::DiagramUsagesReady(completion));
|
||||||
|
let found = f
|
||||||
|
.app
|
||||||
|
.diagram_usages
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.usages
|
||||||
|
.iter()
|
||||||
|
.find(|usage| usage.target == Target::Placement(f.node))
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let _ = f.app.update(Message::DiagramUsage(usage::Event::Choose(
|
||||||
|
token,
|
||||||
|
found.clone(),
|
||||||
|
)));
|
||||||
|
assert!(f.app.pending_diagram_reveal.is_some());
|
||||||
|
assert!(f.app.diagram_focus(found.surface).is_some());
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_stale_noop(app: &mut App, message: Message) {
|
||||||
|
let before = app.document.snapshot();
|
||||||
|
let selection = (app.selected, app.selected_node, app.active_diagram);
|
||||||
|
let camera = (app.zoom, app.pan, app.canvas_epoch, app.camera_epoch);
|
||||||
|
let pending = app.pending_diagram_reveal.clone();
|
||||||
|
let focus = app
|
||||||
|
.diagram_navigation_focus
|
||||||
|
.as_ref()
|
||||||
|
.map(|focus| (focus.context, focus.usage.clone(), focus.selected));
|
||||||
|
let allocation = (app.allocation_drag.epoch, app.allocation_drag.arrange_next);
|
||||||
|
let active = app
|
||||||
|
.allocation_drag
|
||||||
|
.active
|
||||||
|
.as_ref()
|
||||||
|
.map(|active| (active.press.token, active.cache.len()));
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let status = app.status.clone();
|
||||||
|
let _ = app.update(message);
|
||||||
|
assert_eq!(
|
||||||
|
(app.selected, app.selected_node, app.active_diagram),
|
||||||
|
selection
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(app.zoom, app.pan, app.canvas_epoch, app.camera_epoch),
|
||||||
|
camera
|
||||||
|
);
|
||||||
|
assert_eq!(app.pending_diagram_reveal, pending);
|
||||||
|
assert_eq!(
|
||||||
|
app.diagram_navigation_focus.as_ref().map(|focus| (
|
||||||
|
focus.context,
|
||||||
|
focus.usage.clone(),
|
||||||
|
focus.selected
|
||||||
|
)),
|
||||||
|
focus
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(app.allocation_drag.epoch, app.allocation_drag.arrange_next),
|
||||||
|
allocation
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.allocation_drag
|
||||||
|
.active
|
||||||
|
.as_ref()
|
||||||
|
.map(|active| (active.press.token, active.cache.len())),
|
||||||
|
active
|
||||||
|
);
|
||||||
|
assert_eq!(app.source_symbols.interaction, interaction);
|
||||||
|
assert_eq!(app.status, status);
|
||||||
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||||
|
assert_eq!(app.source_workspace.bundle(), before.bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stale_packets(press: &Press) -> Vec<Message> {
|
||||||
|
vec![
|
||||||
|
Message::WorkDrag(Event::Cancel(press.token)),
|
||||||
|
Message::WorkDrag(Event::Preview(press.stamp, press.token, Some(press.start))),
|
||||||
|
Message::WorkDrag(Event::Release(press.stamp, press.token, Some(press.start))),
|
||||||
|
Message::WorkDrag(Event::Refused(press.stamp.epoch, "old refusal".into())),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works() {
|
||||||
|
let mut f = fixture(Layer::Oa);
|
||||||
|
let before = f.app.document.snapshot();
|
||||||
|
let old = press(&f);
|
||||||
|
let _ = f.app.update(Message::WorkDrag(Event::Press(old.clone())));
|
||||||
|
assert_eq!(
|
||||||
|
f.app.allocation_drag.active.as_ref().unwrap().press.token,
|
||||||
|
old.token
|
||||||
|
);
|
||||||
|
let found = navigate_work(&mut f);
|
||||||
|
assert!(f.app.allocation_drag.active.is_none());
|
||||||
|
assert_ne!(f.app.allocation_drag.epoch, old.stamp.epoch);
|
||||||
|
for message in stale_packets(&old) {
|
||||||
|
assert_stale_noop(&mut f.app, message);
|
||||||
|
}
|
||||||
|
// A rejected press still explains the invalid view, but cannot consume the
|
||||||
|
// new destination's pending first measurement.
|
||||||
|
let pending = f.app.pending_diagram_reveal.clone();
|
||||||
|
let epoch = f.app.allocation_drag.epoch;
|
||||||
|
let _ = f.app.update(Message::WorkDrag(Event::Press(old.clone())));
|
||||||
|
assert_eq!(f.app.pending_diagram_reveal, pending);
|
||||||
|
assert_eq!(f.app.allocation_drag.epoch, epoch);
|
||||||
|
assert!(f.app.diagram_focus(found.surface).is_some());
|
||||||
|
assert_eq!(f.app.status, "The view changed. Start this drag again.");
|
||||||
|
let (context, _, epoch) = f.app.pending_diagram_reveal.clone().unwrap();
|
||||||
|
let _ = f.app.update(Message::DiagramRevealViewport(
|
||||||
|
context,
|
||||||
|
epoch,
|
||||||
|
bounds().size(),
|
||||||
|
));
|
||||||
|
assert!(f.app.pending_diagram_reveal.is_none());
|
||||||
|
assert!(f.app.diagram_focus(found.surface).is_some());
|
||||||
|
|
||||||
|
// Restore the fixture's camera through current App actions, then capture a
|
||||||
|
// fresh packet through the actual DiagramCanvas rather than seeding active.
|
||||||
|
let factor = 1. / f.app.zoom;
|
||||||
|
let _ = f.app.update(Message::Zoom(factor));
|
||||||
|
let delta = -f.app.pan;
|
||||||
|
let _ = f.app.update(Message::Pan(delta));
|
||||||
|
let current = press(&f);
|
||||||
|
let _ = f
|
||||||
|
.app
|
||||||
|
.update(Message::WorkDrag(Event::Press(current.clone())));
|
||||||
|
assert_eq!(
|
||||||
|
f.app.allocation_drag.active.as_ref().unwrap().press.token,
|
||||||
|
current.token
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
f.app.diagram_navigation_focus.is_none(),
|
||||||
|
"live press retires navigation"
|
||||||
|
);
|
||||||
|
for message in stale_packets(&old) {
|
||||||
|
assert_stale_noop(&mut f.app, message);
|
||||||
|
}
|
||||||
|
let epoch = f.app.allocation_drag.epoch;
|
||||||
|
let _ = f
|
||||||
|
.app
|
||||||
|
.update(Message::WorkDrag(Event::Cancel(current.token)));
|
||||||
|
assert!(f.app.allocation_drag.active.is_none());
|
||||||
|
assert!(f.app.allocation_drag.epoch > epoch);
|
||||||
|
assert_eq!(f.app.status, "Work drag canceled");
|
||||||
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
||||||
|
assert_eq!(f.app.source_workspace.bundle(), before.bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn old_chooser_envelopes_preserve_a_current_canvas_gesture() {
|
||||||
|
let mut previous = fixture(Layer::Oa);
|
||||||
|
let old_open = previous.app.diagram_usage_message(previous.work);
|
||||||
|
let _ = previous.app.update(old_open.clone());
|
||||||
|
let old_token = previous.app.diagram_usages.as_ref().unwrap().token;
|
||||||
|
let mut f = fixture(Layer::Oa);
|
||||||
|
let current = press(&f);
|
||||||
|
let _ = f
|
||||||
|
.app
|
||||||
|
.update(Message::WorkDrag(Event::Press(current.clone())));
|
||||||
|
assert_stale_noop(&mut f.app, old_open);
|
||||||
|
assert_stale_noop(
|
||||||
|
&mut f.app,
|
||||||
|
Message::DiagramUsage(usage::Event::Close(old_token)),
|
||||||
|
);
|
||||||
|
assert_stale_noop(
|
||||||
|
&mut f.app,
|
||||||
|
Message::DiagramUsage(usage::Event::Search(old_token, "old".into())),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
f.app.allocation_drag.active.as_ref().unwrap().press.token,
|
||||||
|
current.token
|
||||||
|
);
|
||||||
|
let open = f.app.diagram_usage_message(f.work);
|
||||||
|
let _ = f.app.update(open);
|
||||||
|
assert!(
|
||||||
|
f.app.diagram_usages.is_some(),
|
||||||
|
"current chooser remains live"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
f.app.allocation_drag.active.is_none(),
|
||||||
|
"current chooser retires gesture"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -144,14 +144,31 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn diagram_usage_event(&mut self, event: Event) -> Task<Message> {
|
pub(super) fn diagram_usage_envelope(&self, event: &Event) -> bool {
|
||||||
if let Event::Open(context) = event {
|
match event {
|
||||||
if context.document != self.canvas_document
|
Event::Open(context) => {
|
||||||
|| context.revision != self.document.revision()
|
context.document == self.canvas_document
|
||||||
|| !self.project().contains(context.subject)
|
&& context.revision == self.document.revision()
|
||||||
{
|
&& self.project().contains(context.subject)
|
||||||
return Task::none();
|
|
||||||
}
|
}
|
||||||
|
Event::Close(token)
|
||||||
|
| Event::Search(token, _)
|
||||||
|
| Event::Page(token, _)
|
||||||
|
| Event::Move(token, _)
|
||||||
|
| Event::Activate(token)
|
||||||
|
| Event::Destination(token, _)
|
||||||
|
| Event::Choose(token, _) => self
|
||||||
|
.diagram_usages
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|panel| panel.token == *token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn diagram_usage_event(&mut self, event: Event) -> Task<Message> {
|
||||||
|
if !self.diagram_usage_envelope(&event) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if let Event::Open(context) = event {
|
||||||
if !self.usage_navigation_available() {
|
if !self.usage_navigation_available() {
|
||||||
self.status = "Apply or revert pending edits before opening diagram navigation. Your inputs are retained.".into();
|
self.status = "Apply or revert pending edits before opening diagram navigation. Your inputs are retained.".into();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -171,22 +188,10 @@ impl App {
|
|||||||
iced::widget::operation::focus(filter_id),
|
iced::widget::operation::focus(filter_id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
let token = match &event {
|
let panel = self
|
||||||
Event::Open(_) => unreachable!(),
|
.diagram_usages
|
||||||
Event::Close(t)
|
.as_ref()
|
||||||
| Event::Search(t, _)
|
.expect("admitted chooser token");
|
||||||
| Event::Page(t, _)
|
|
||||||
| Event::Move(t, _)
|
|
||||||
| Event::Activate(t)
|
|
||||||
| Event::Destination(t, _)
|
|
||||||
| Event::Choose(t, _) => *t,
|
|
||||||
};
|
|
||||||
let Some(panel) = &self.diagram_usages else {
|
|
||||||
return Task::none();
|
|
||||||
};
|
|
||||||
if panel.token != token {
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
if matches!(event, Event::Close(_)) {
|
if matches!(event, Event::Close(_)) {
|
||||||
self.diagram_usages = None;
|
self.diagram_usages = None;
|
||||||
if self.page == Page::Text {
|
if self.page == Page::Text {
|
||||||
|
|||||||
@@ -1029,10 +1029,9 @@ fn repeated_preview_arrows_select_the_relation_and_highlight_the_exact_caller_li
|
|||||||
let camera = app.behavior.topology.camera_boundary();
|
let camera = app.behavior.topology.camera_boundary();
|
||||||
if call == calls[1]
|
if call == calls[1]
|
||||||
&& let Some((previous_call, previous)) = &previous_camera
|
&& let Some((previous_call, previous)) = &previous_camera
|
||||||
|
&& *previous_call != call
|
||||||
{
|
{
|
||||||
if *previous_call != call {
|
assert!(&camera != previous);
|
||||||
assert!(&camera != previous);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(epoch) = previous_epoch {
|
if let Some(epoch) = previous_epoch {
|
||||||
assert_ne!(app.canvas_epoch, epoch);
|
assert_ne!(app.canvas_epoch, epoch);
|
||||||
@@ -1375,7 +1374,7 @@ fn whole_surface_camera_fits_huge_extent_while_target_camera_remains_readable()
|
|||||||
};
|
};
|
||||||
let viewport = iced::Size::new(800., 500.);
|
let viewport = iced::Size::new(800., 500.);
|
||||||
let (zoom, pan) = usage::camera_for(&Target::Whole, bounds, viewport);
|
let (zoom, pan) = usage::camera_for(&Target::Whole, bounds, viewport);
|
||||||
assert!(zoom < 0.05 && zoom >= 0.0001);
|
assert!((0.0001..0.05).contains(&zoom));
|
||||||
assert!(bounds.width * zoom <= viewport.width - 64. + 0.01);
|
assert!(bounds.width * zoom <= viewport.width - 64. + 0.01);
|
||||||
assert!(bounds.height * zoom <= viewport.height - 64. + 0.01);
|
assert!(bounds.height * zoom <= viewport.height - 64. + 0.01);
|
||||||
assert!((bounds.center_x() * zoom + pan.x - viewport.width / 2.).abs() < 0.01);
|
assert!((bounds.center_x() * zoom + pan.x - viewport.width / 2.).abs() < 0.01);
|
||||||
|
|||||||
@@ -1,6 +1,100 @@
|
|||||||
//! Interactions absent from the independently published navigation branch.
|
//! Interactions absent from the independently published navigation branch.
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn assert_stale_navigation_message(message: Message) {
|
||||||
|
let mut app = navigation_fixture();
|
||||||
|
let found = navigate(&mut app);
|
||||||
|
let destination = navigation(&app);
|
||||||
|
let allocation_epoch = app.allocation_drag.epoch;
|
||||||
|
let accepted = app.document.snapshot();
|
||||||
|
let status = app.status.clone();
|
||||||
|
send(&mut app, message);
|
||||||
|
assert_eq!(navigation(&app), destination);
|
||||||
|
assert_eq!(app.allocation_drag.epoch, allocation_epoch);
|
||||||
|
assert_eq!(app.status, status);
|
||||||
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||||
|
assert_eq!(app.source_workspace.bundle(), accepted.bundle);
|
||||||
|
first_measurement(&mut app, &found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captured_old_source_query_and_find_close_preserve_the_new_destination() {
|
||||||
|
let mut previous = navigation_fixture();
|
||||||
|
let query = previous.source_symbol_message(nav::SymbolEvent::Query(query::Action::Definition));
|
||||||
|
send(
|
||||||
|
&mut previous,
|
||||||
|
Message::SourceNavigation(nav::Event::Open(false)),
|
||||||
|
);
|
||||||
|
assert!(previous.source_navigation.open);
|
||||||
|
let close = previous.source_find_close_message();
|
||||||
|
assert!(previous.source_input_envelope(&close));
|
||||||
|
assert_stale_navigation_message(query);
|
||||||
|
assert_stale_navigation_message(close);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_source_query_and_checked_find_close_remain_live() {
|
||||||
|
let mut app = navigation_fixture();
|
||||||
|
let before = app.document.snapshot();
|
||||||
|
send(&mut app, Message::SourceNavigation(nav::Event::Open(false)));
|
||||||
|
assert!(app.source_navigation.open);
|
||||||
|
let close = app.source_find_close_message();
|
||||||
|
send(&mut app, close.clone());
|
||||||
|
assert!(!app.source_navigation.open);
|
||||||
|
let epoch = app.allocation_drag.epoch;
|
||||||
|
send(&mut app, close);
|
||||||
|
assert_eq!(app.allocation_drag.epoch, epoch, "retired close is inert");
|
||||||
|
let message = app.source_symbol_message(nav::SymbolEvent::Query(query::Action::Definition));
|
||||||
|
send(&mut app, message);
|
||||||
|
assert!(app.source_symbols.in_flight.is_some());
|
||||||
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||||
|
assert_eq!(app.source_workspace.bundle(), before.bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captured_old_chooser_context_and_every_token_variant_are_inert() {
|
||||||
|
let mut previous = navigation_fixture();
|
||||||
|
let open = previous.diagram_usage_message(model_id(2));
|
||||||
|
send(&mut previous, open.clone());
|
||||||
|
let (token, found) = finish_lookup(&mut previous);
|
||||||
|
assert_stale_navigation_message(open);
|
||||||
|
for event in [
|
||||||
|
usage::Event::Close(token),
|
||||||
|
usage::Event::Search(token, "old filter".into()),
|
||||||
|
usage::Event::Page(token, 1),
|
||||||
|
usage::Event::Move(token, 1),
|
||||||
|
usage::Event::Activate(token),
|
||||||
|
usage::Event::Destination(token, Some(found.surface)),
|
||||||
|
usage::Event::Choose(token, found),
|
||||||
|
] {
|
||||||
|
assert_stale_navigation_message(Message::DiagramUsage(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_chooser_open_and_close_keep_their_boundary_and_worker_semantics() {
|
||||||
|
let mut app = navigation_fixture();
|
||||||
|
navigate(&mut app);
|
||||||
|
let epoch = app.allocation_drag.epoch;
|
||||||
|
let message = app.diagram_usage_message(model_id(2));
|
||||||
|
send(&mut app, message);
|
||||||
|
assert!(app.diagram_usages.is_some());
|
||||||
|
assert!(app.pending_diagram_reveal.is_none());
|
||||||
|
assert!(app.diagram_navigation_focus.is_none());
|
||||||
|
assert!(app.allocation_drag.epoch > epoch);
|
||||||
|
let completion = diagram_completion(&app);
|
||||||
|
let token = completion.token;
|
||||||
|
send(&mut app, Message::DiagramUsage(usage::Event::Close(token)));
|
||||||
|
assert!(app.diagram_usages.is_none());
|
||||||
|
assert_eq!(app.diagram_lookup_in_flight, Some(token));
|
||||||
|
send(&mut app, Message::DiagramUsagesReady(completion));
|
||||||
|
assert!(app.diagram_lookup_in_flight.is_none());
|
||||||
|
assert!(
|
||||||
|
app.diagram_usages.is_none(),
|
||||||
|
"completion does not reopen a closed chooser"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn allocation_keyboard_notification_preserves_navigation_and_first_measurement() {
|
fn allocation_keyboard_notification_preserves_navigation_and_first_measurement() {
|
||||||
let mut app = navigation_fixture();
|
let mut app = navigation_fixture();
|
||||||
|
|||||||
@@ -31,6 +31,24 @@ fn admission_notice(error: query::AdmissionError) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
pub(super) fn source_input_envelope(&self, message: &Message) -> bool {
|
||||||
|
match message {
|
||||||
|
Message::SourceSymbols(context, _) => {
|
||||||
|
context.session == self.source_workspace.session()
|
||||||
|
&& context.path == self.source_workspace.active
|
||||||
|
}
|
||||||
|
Message::SourceFindClose(context, instance) => {
|
||||||
|
context == &self.source_symbol_context()
|
||||||
|
&& *instance == self.source_find_instance
|
||||||
|
&& self.source_navigation.open
|
||||||
|
&& self.source_navigation_available()
|
||||||
|
}
|
||||||
|
// The checked Find-close envelope is mapped inside update_inner.
|
||||||
|
Message::SourceNavigation(nav::Event::Close) => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn source_symbol_context(&self) -> nav::SymbolContext {
|
pub fn source_symbol_context(&self) -> nav::SymbolContext {
|
||||||
nav::SymbolContext {
|
nav::SymbolContext {
|
||||||
session: self.source_workspace.session(),
|
session: self.source_workspace.session(),
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody)
|
|||||||
precondition: String::new(),
|
precondition: String::new(),
|
||||||
postcondition: String::new(),
|
postcondition: String::new(),
|
||||||
};
|
};
|
||||||
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() {
|
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut()
|
||||||
if !steps.contains(&id) {
|
&& !steps.contains(&id)
|
||||||
steps.push(id);
|
{
|
||||||
}
|
steps.push(id);
|
||||||
}
|
}
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,3 +56,29 @@ Only source inspection, bounded formatting and whitespace checks are performed h
|
|||||||
No `cargo`, `rustc`, test binary or GUI was run. Compile/lint correctness, new test outcomes, timing and actual native usability remain open. The integration owner should run focused new filters first, retain existing grouped/full/strict gates and build the matching executable. The parent's e44 results do not validate this union.
|
No `cargo`, `rustc`, test binary or GUI was run. Compile/lint correctness, new test outcomes, timing and actual native usability remain open. The integration owner should run focused new filters first, retain existing grouped/full/strict gates and build the matching executable. The parent's e44 results do not validate this union.
|
||||||
|
|
||||||
Native acceptance retains the peer's six-file `examples/source-project` oracle: rename the second `@assess_report` reference to `assess_report_union`, exactly six alias-token changes in two files, no disk change before Save, immediate editor selection/typing and text Undo, navigation without model history, complete model Undo/Redo, then Save/reopen equality. Also exercise actual overflowing preview/Details return, modal exclusivity, source syntax selection, canvas focus/Delete after navigation, exact port/state-row and repeated-placement targets, nested activation shoulders/lifecycle boundaries, and passive event timing. Use deterministic race tests for held operations; screenshots alone do not establish ordering. Linux remains deferred.
|
Native acceptance retains the peer's six-file `examples/source-project` oracle: rename the second `@assess_report` reference to `assess_report_union`, exactly six alias-token changes in two files, no disk change before Save, immediate editor selection/typing and text Undo, navigation without model history, complete model Undo/Redo, then Save/reopen equality. Also exercise actual overflowing preview/Details return, modal exclusivity, source syntax selection, canvas focus/Delete after navigation, exact port/state-row and repeated-placement targets, nested activation shoulders/lifecycle boundaries, and passive event timing. Use deterministic race tests for held operations; screenshots alone do not establish ordering. Linux remains deferred.
|
||||||
|
|
||||||
|
## Independent-review correction after 4e51, source only
|
||||||
|
|
||||||
|
The independent review of `4e51d68973da52cf057fab4983b9f112400b802c` identified an actual dispatch-order defect: old source, chooser and allocation envelopes reached shared reveal/allocation boundaries before their handlers rejected them. The follow-up shares those existing admission checks with the outer App boundary. SourceSymbols retains session/path checks; Find-close retains context/instance/open/availability checks and maps to the legacy Close only inside the checked handler. DiagramUsage retains exact Open document/revision/subject checks and all seven panel-token checks. Cancel, Preview and Release retain the active gesture token check; Refused retains its epoch check. Worker completion handlers and their exact occupied-slot release remain unchanged.
|
||||||
|
|
||||||
|
Rejected Press packets still run their existing feedback handler, before shared boundaries, so an old stamp can explain the changed view without consuming a newer destination. Current gestures, current chooser actions and current Find-close remain active. Current-token chooser model-refresh recovery is retained; this change does not require a matching panel revision before its existing refresh handler can run. No selection rewrite or broader event classification is introduced.
|
||||||
|
|
||||||
|
The exact test-only peer correction `a49b0ec7ac4538a372139175386a881645c1a59e` was inspected and applied: two collapsed conditions and one range containment expression in `app/diagram_usages/tests.rs` and `behavior/scenario/usage/tests.rs`. A source sweep also supplies the newly required `navigation_focus: None` in the root allocation test canvas and the production dirty-point-only canvas in `allocation_input.rs`. The latter is used only for the dirty-work hit check, not painted.
|
||||||
|
|
||||||
|
Four further tests under `app::source_rename::tests::navigation_union_tests::palette_union::`, all unrun:
|
||||||
|
|
||||||
|
- `captured_old_source_query_and_find_close_preserve_the_new_destination`
|
||||||
|
- `current_source_query_and_checked_find_close_remain_live`
|
||||||
|
- `captured_old_chooser_context_and_every_token_variant_are_inert`
|
||||||
|
- `current_chooser_open_and_close_keep_their_boundary_and_worker_semantics`
|
||||||
|
|
||||||
|
They capture actual prior owners/tokens and check complete navigation, allocation epoch, source interaction, status and accepted snapshot retention; each rejected navigation case then completes the valid pending viewport. Positive controls exercise current source query/Find-close and current chooser boundaries, including close while its worker slot remains occupied and exact completion release.
|
||||||
|
|
||||||
|
Two further tests under `app::allocation_drag::tests::navigation_union::`, all unrun:
|
||||||
|
|
||||||
|
- `stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works`
|
||||||
|
- `old_chooser_envelopes_preserve_a_current_canvas_gesture`
|
||||||
|
|
||||||
|
They capture Press through the actual DiagramCanvas event path, navigate through the actual chooser, preserve camera/selection/focus/pending measurement and the newer active gesture across stale packets, retain invalid-Press feedback, and exercise current Cancel and current chooser retirement. They do not seed private active gesture state. The resulting `palette_union::` filter contains seven tests, its enclosing `navigation_union_tests::` contains thirteen, the separate removal union still contains two, and the new allocation union contains two. The four existing preview-scroll tests are retained.
|
||||||
|
|
||||||
|
Only bounded rustfmt and whitespace/source checks are performed for this follow-up; the ignored formatting receipt is `.local/navigation-import/stale-envelope-format.json`. All new tests, compilation, strict lint and native acceptance remain unrun here. Root readonly, typed activation/lifecycle and paint implementations are untouched by this follow-up. The integration owner must execute the combined source gates; neither source review nor peer-only passes establishes union acceptance.
|
||||||
|
|||||||
Reference in New Issue
Block a user