Keep observed scenario scrolling current during async saves

This commit is contained in:
2026-09-06 04:58:45 +03:00
parent 5fa9c905c8
commit e7e37d485d
2 changed files with 112 additions and 0 deletions
+9
View File
@@ -949,6 +949,15 @@ impl App {
]) ])
} }
pub fn update(&mut self, message: Message) -> Task<Message> { pub fn update(&mut self, message: Message) -> Task<Message> {
// Scrollable geometry can change while async work or an overlay blocks
// commands. Keep the observed translation current; its passive handler
// still validates document, scenario and revision before accepting it.
if matches!(
&message,
Message::ScenarioCanvas(crate::behavior::scenario::CanvasEvent::Scrolled(..))
) {
return self.handle(message);
}
// Global subscriptions also receive keys consumed by an overlay. // Global subscriptions also receive keys consumed by an overlay.
if self.model_menu.is_some() && matches!(message, Message::Escape) { if self.model_menu.is_some() && matches!(message, Message::Escape) {
self.model_menu = None; self.model_menu = None;
+103
View File
@@ -1813,6 +1813,109 @@ pub(crate) mod direct_tests {
); );
} }
#[test] #[test]
fn direct_busy_save_keeps_observed_scroll_current_for_the_next_pointer_gesture() {
let f = fixture();
let mut app = App::with_project(f.project.clone());
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let context = app.scenario_context().unwrap();
let original = app.document.snapshot();
app.busy = true;
for (area, offset) in [
(ScrollArea::Canvas, iced::Vector::new(25., 100.)),
(ScrollArea::Page, iced::Vector::new(0., 40.)),
] {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context, area, offset,
)));
}
assert!(app.busy);
assert_eq!(
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 140.)
);
assert_ne!(
app.scenario_context(),
Some(context),
"scroll invalidates the pre-save pointer context even while busy"
);
let mut stale = context;
stale.document = Id::new_v4();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
stale,
ScrollArea::Canvas,
iced::Vector::new(0., 900.),
)));
assert_eq!(
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 140.)
);
let root = std::path::PathBuf::from("scenario-scroll-save-fixture");
let receipt = crate::app::FileSnapshot {
document: context.document,
revision: context.revision,
snapshot: original.clone(),
path: root.join(&original.bundle.entry),
disk: Some(oa_core::source_files::DiskProject {
root,
bundle: original.bundle.clone(),
expectations: BTreeMap::new(),
reachable: original.bundle.files.keys().cloned().collect(),
diagnostics: vec![],
}),
};
let _ = app.update(Message::Saved(Ok(Some(receipt))));
assert!(!app.busy);
assert_eq!(app.project(), &original.decoded.project);
assert!(!app.document.can_undo());
let context = app.scenario_context().unwrap();
let mut scene = Scene::sequence(app.project(), f.owner, f.call);
scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true));
scene.scroll_offset = app.behavior.scenario.scroll_offset();
let slot = scene.slots.last().unwrap().slot;
let bounds = Rectangle::new(Point::new(20., 250.), scene.size);
let y = bounds.y + scene.slots.last().unwrap().y;
let a = Point::new(bounds.x + scene.lanes[&f.sender], y) - scene.scroll_offset;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) - scene.scroll_offset;
let mut input = super::super::SequenceInput::default();
let mut published = vec![];
for event in [
mouse::Event::CursorMoved { position: a },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: b },
mouse::Event::ButtonReleased(mouse::Button::Left),
] {
if let Some(message) = canvas::Program::update(
&scene,
&mut input,
&canvas::Event::Mouse(event),
bounds,
mouse::Cursor::Available(b + scene.scroll_offset),
)
.and_then(|action| action.into_inner().0)
{
published.push(message);
}
}
assert_eq!(published.len(), 1);
assert!(
matches!(&published[0], Message::ScenarioCanvas(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if *sender == f.sender && *receiver == f.receiver && *actual == slot)
);
let _ = app.update(published.pop().unwrap());
assert_eq!(
app.behavior.scenario.draft.as_ref().unwrap().before,
slot.before
);
assert_eq!(
app.behavior.scenario.draft.as_ref().unwrap().sender,
Some(f.sender)
);
assert_eq!(
app.behavior.scenario.draft.as_ref().unwrap().receiver,
Some(f.receiver)
);
}
#[test]
fn direct_reply_choices_cache_keeps_typing_fast_and_revalidates_changed_contexts() { fn direct_reply_choices_cache_keeps_typing_fast_and_revalidates_changed_contexts() {
let f = fixture(); let f = fixture();
let mut p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap(); let mut p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap();