Fix scenario scroll cancellation, operand activations and picker cost
This commit is contained in:
@@ -21,6 +21,297 @@ pub(crate) mod state_menu;
|
||||
pub(crate) mod state_view;
|
||||
pub(crate) mod states;
|
||||
pub(crate) mod topology;
|
||||
// The standard on_scroll callback only reports active scrolling. This local
|
||||
// wrapper reads the existing Scrollable's actual translation as well, including
|
||||
// initial mount and passive size clamping. It forwards layout, input and drawing.
|
||||
mod scenario_scroll {
|
||||
use super::*;
|
||||
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, renderer, widget};
|
||||
use iced::{Length, Vector, overlay};
|
||||
|
||||
pub(super) fn observe<'a>(
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
Element::new(Observer {
|
||||
content: content.into(),
|
||||
context,
|
||||
area,
|
||||
})
|
||||
}
|
||||
struct Observer<'a, R = Renderer> {
|
||||
content: Element<'a, Message, Theme, R>,
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
}
|
||||
type Report = Option<(Id, Id, Vector)>;
|
||||
#[derive(Default)]
|
||||
struct Translation(Option<Vector>);
|
||||
impl widget::Operation for Translation {
|
||||
fn traverse(&mut self, _: &mut dyn FnMut(&mut dyn widget::Operation)) {}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
_: Rectangle,
|
||||
translation: Vector,
|
||||
_: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
self.0 = Some(translation);
|
||||
}
|
||||
}
|
||||
impl<R: renderer::Renderer> Observer<'_, R> {
|
||||
fn report(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &R,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
let mut offset = Translation::default();
|
||||
self.content.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
&mut offset,
|
||||
);
|
||||
if let Some(offset) = offset.0 {
|
||||
let next = Some((self.context.document, self.context.scenario, offset));
|
||||
let previous = tree.state.downcast_mut::<Report>();
|
||||
if *previous != next {
|
||||
*previous = next;
|
||||
shell.publish(Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
|
||||
self.context,
|
||||
self.area,
|
||||
offset,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<R: renderer::Renderer> Widget<Message, Theme, R> for Observer<'_, R> {
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
widget::tree::Tag::of::<Report>()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(Report::None)
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
vec![widget::Tree::new(self.content.as_widget())]
|
||||
}
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.content));
|
||||
}
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &R,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &iced::Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &R,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
// A passive clamp notification precedes any pointer result produced
|
||||
// with the old context; App rejects that result after the epoch bump.
|
||||
self.report(tree, layout, renderer, shell);
|
||||
self.content.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
self.report(tree, layout, renderer, shell);
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut R,
|
||||
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 operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &R,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &R,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut widget::Tree,
|
||||
layout: Layout<'b>,
|
||||
renderer: &R,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, R>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Iced exposes its null renderer only in debug builds.
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn observer(height: f32, context: scenario::Context) -> Observer<'static, ()> {
|
||||
Observer {
|
||||
context,
|
||||
area: scenario::ScrollArea::Canvas,
|
||||
content: scrollable(iced::widget::Space::new().width(200.).height(height))
|
||||
.width(200.)
|
||||
.height(200.)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
fn update(
|
||||
observer: &mut Observer<'_, ()>,
|
||||
tree: &mut widget::Tree,
|
||||
event: iced::Event,
|
||||
) -> (Vec<Message>, bool) {
|
||||
let node = observer.layout(
|
||||
tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
|
||||
);
|
||||
let mut messages = vec![];
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
observer.update(
|
||||
tree,
|
||||
&event,
|
||||
Layout::new(&node),
|
||||
mouse::Cursor::Available(Point::new(50., 50.)),
|
||||
&(),
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(Size::new(800., 700.)),
|
||||
);
|
||||
let captured = shell.is_event_captured();
|
||||
(messages, captured)
|
||||
}
|
||||
fn offsets(messages: &[Message]) -> Vec<Vector> {
|
||||
messages
|
||||
.iter()
|
||||
.filter_map(|message| {
|
||||
if let Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(_, _, offset)) =
|
||||
message
|
||||
{
|
||||
Some(*offset)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
#[test]
|
||||
fn direct_scroll_observer_reports_actual_initial_clamped_and_remounted_offsets_once() {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let redraw = || {
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
))
|
||||
};
|
||||
let mut view = observer(600., context);
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
assert_eq!(
|
||||
offsets(&update(&mut view, &mut tree, redraw()).0),
|
||||
vec![Vector::ZERO]
|
||||
);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
let (messages, captured) = update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
|
||||
}),
|
||||
);
|
||||
assert!(captured, "the observer forwards normal Scrollable capture");
|
||||
assert_eq!(offsets(&messages), vec![Vector::new(0., 180.)]);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
view = observer(250., context);
|
||||
view.diff(&mut tree);
|
||||
assert_eq!(
|
||||
offsets(&update(&mut view, &mut tree, redraw()).0),
|
||||
vec![Vector::new(0., 50.)]
|
||||
);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
view = observer(600., context);
|
||||
view.diff(&mut tree);
|
||||
assert_eq!(
|
||||
offsets(&update(&mut view, &mut tree, redraw()).0),
|
||||
vec![Vector::new(0., 180.)]
|
||||
);
|
||||
let mut remounted = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
assert_eq!(
|
||||
offsets(&update(&mut view, &mut remounted, redraw()).0),
|
||||
vec![Vector::ZERO]
|
||||
);
|
||||
assert!(update(&mut view, &mut remounted, redraw()).0.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Topology(topology::Event),
|
||||
@@ -254,11 +545,13 @@ struct Scene {
|
||||
size: Size,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
scroll_offset: iced::Vector,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
slots: Vec<SequenceSlot>,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
open_calls: Vec<Point>,
|
||||
sequence_bounds: BTreeMap<Id, (f32, f32)>,
|
||||
}
|
||||
struct SequenceSlot {
|
||||
slot: scenario::Slot,
|
||||
@@ -270,6 +563,7 @@ struct SequenceInput {
|
||||
context: Option<scenario::Context>,
|
||||
tool: scenario::CanvasTool,
|
||||
pointer: Option<Point>,
|
||||
bounds: Option<Rectangle>,
|
||||
source: Option<(Id, scenario::Slot)>,
|
||||
press: Option<SequencePress>,
|
||||
}
|
||||
@@ -299,6 +593,7 @@ impl Scene {
|
||||
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
||||
let mut scene = Self {
|
||||
scale: 1.,
|
||||
scroll_offset: iced::Vector::ZERO,
|
||||
boxes: vec![],
|
||||
lines: vec![],
|
||||
size: Size::new(700., 200.),
|
||||
@@ -308,6 +603,7 @@ impl Scene {
|
||||
slots: vec![],
|
||||
rows: BTreeMap::new(),
|
||||
open_calls: vec![],
|
||||
sequence_bounds: BTreeMap::new(),
|
||||
};
|
||||
let mut lanes = p
|
||||
.children(owner)
|
||||
@@ -395,50 +691,28 @@ impl Scene {
|
||||
let mut y = if scenario { 148. } else { 120. };
|
||||
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0);
|
||||
if scenario {
|
||||
for (&id, &(_, top)) in &scene.rows {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Call,
|
||||
receiver: Some(receiver),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = p.elements[&id].shared()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(x) = lane_x.get(receiver) else {
|
||||
continue;
|
||||
};
|
||||
let ends: Vec<_> = scene.rows.iter().filter_map(|(reply, (_, y))| {
|
||||
matches!(p.elements[reply].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Reply, reply_to: Some(call), .. }, .. }) if *call == id && *y > top).then_some(*y)
|
||||
}).collect();
|
||||
let bottom = ends
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(f32::total_cmp)
|
||||
.unwrap_or(top + 26.);
|
||||
scene.boxes.push(BoxItem {
|
||||
id,
|
||||
rect: Rectangle::new(Point::new(*x - 4., top), Size::new(8., bottom - top)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
if ends.is_empty() {
|
||||
scene.open_calls.push(Point::new(*x + 8., bottom - 7.));
|
||||
}
|
||||
for end in ends {
|
||||
scene.boxes.push(BoxItem {
|
||||
id,
|
||||
rect: Rectangle::new(Point::new(*x - 7., end - 1.), Size::new(14., 2.)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
}
|
||||
let calls: Vec<_> = scene
|
||||
.rows
|
||||
.keys()
|
||||
.filter_map(|&id| {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Call,
|
||||
receiver: Some(receiver),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = p.elements[&id].shared()
|
||||
{
|
||||
Some((id, *lane_x.get(receiver)?))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for (call, x) in calls {
|
||||
scene.activation_sequence(p, p.elements[&call].parent.unwrap(), call, x, None);
|
||||
}
|
||||
}
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
@@ -491,6 +765,7 @@ impl Scene {
|
||||
})
|
||||
.unwrap_or(&[]);
|
||||
let ordered = p.elements.get(&owner).and_then(|e| e.steps()).is_some();
|
||||
let sequence_top = *y - 42.;
|
||||
for (index, id) in steps.iter().enumerate() {
|
||||
let Some(e) = p.elements.get(id) else {
|
||||
continue;
|
||||
@@ -696,6 +971,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
if ordered {
|
||||
self.sequence_bounds.insert(owner, (sequence_top, *y - 42.));
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
before: None,
|
||||
@@ -707,6 +983,106 @@ impl Scene {
|
||||
});
|
||||
}
|
||||
}
|
||||
fn activation_segment(&mut self, call: Id, x: f32, top: f32, bottom: f32) {
|
||||
if bottom <= top {
|
||||
return;
|
||||
}
|
||||
self.boxes.push(BoxItem {
|
||||
id: call,
|
||||
rect: Rectangle::new(Point::new(x - 4., top), Size::new(8., bottom - top)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
}
|
||||
/// Follow a single correlation through the ordered tree. Branches are drawn
|
||||
/// as independent segments, never as one chronological stretch through ALT
|
||||
/// operands. The result says whether a path can leave this sequence open.
|
||||
fn activation_sequence(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
owner: Id,
|
||||
call: Id,
|
||||
x: f32,
|
||||
mut start: Option<f32>,
|
||||
) -> bool {
|
||||
let entered_open = start.is_some();
|
||||
let mut split = false;
|
||||
for id in p.elements[&owner].steps().into_iter().flatten() {
|
||||
let Some(&(_, y)) = self.rows.get(id) else {
|
||||
continue;
|
||||
};
|
||||
if *id == call {
|
||||
start = Some(y);
|
||||
}
|
||||
let Some(top) = start else {
|
||||
continue;
|
||||
};
|
||||
match p.elements[id].shared() {
|
||||
Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Reply,
|
||||
reply_to: Some(correlation),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) if *correlation == call => {
|
||||
self.activation_segment(call, x, top, y);
|
||||
self.boxes.push(BoxItem {
|
||||
id: call,
|
||||
rect: Rectangle::new(Point::new(x - 7., y - 1.), Size::new(14., 2.)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
start = None;
|
||||
}
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { operator, branches },
|
||||
..
|
||||
}) => {
|
||||
split = true;
|
||||
self.activation_segment(call, x, top, y);
|
||||
let bottom = self
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|b| b.id == *id && !b.title.is_empty())
|
||||
.map(|b| b.rect.y + b.rect.height)
|
||||
.unwrap_or(y);
|
||||
let outcomes: Vec<_> = branches
|
||||
.iter()
|
||||
.filter_map(|branch| {
|
||||
let &(top, _) = self.sequence_bounds.get(branch)?;
|
||||
Some(self.activation_sequence(p, *branch, call, x, Some(top)))
|
||||
})
|
||||
.collect();
|
||||
let open = match operator {
|
||||
FragmentOperator::Alternative => {
|
||||
outcomes.is_empty() || outcomes.iter().any(|open| *open)
|
||||
}
|
||||
FragmentOperator::Parallel => outcomes.iter().all(|open| *open),
|
||||
// Optional/repeated bodies do not guarantee execution.
|
||||
FragmentOperator::Optional | FragmentOperator::Loop => true,
|
||||
};
|
||||
start = open.then_some(bottom);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(top) = start {
|
||||
let bottom = if entered_open || split {
|
||||
self.sequence_bounds
|
||||
.get(&owner)
|
||||
.map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.))
|
||||
} else {
|
||||
top + 26.
|
||||
};
|
||||
self.activation_segment(call, x, top, bottom);
|
||||
self.open_calls.push(Point::new(x + 8., bottom - 7.));
|
||||
}
|
||||
start.is_some()
|
||||
}
|
||||
fn slot_at(&self, point: Point, parent: Option<Id>) -> Option<&SequenceSlot> {
|
||||
self.slots
|
||||
.iter()
|
||||
@@ -807,6 +1183,14 @@ impl canvas::Program<Message> for Scene {
|
||||
state.context = context;
|
||||
state.tool = tool;
|
||||
}
|
||||
// Scrolling either enclosing Scrollable changes the canvas origin. A
|
||||
// captured insertion slot belongs to the previous view, even if the
|
||||
// pointer itself did not move and the semantic context is unchanged.
|
||||
if state.bounds.is_some_and(|previous| previous != bounds) {
|
||||
state.cancel();
|
||||
state.pointer = None;
|
||||
}
|
||||
state.bounds = Some(bounds);
|
||||
if !editable {
|
||||
state.cancel();
|
||||
}
|
||||
@@ -833,9 +1217,11 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
mouse::Cursor::Available(*position)
|
||||
mouse::Cursor::Available(*position + self.scroll_offset)
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::CursorLeft)
|
||||
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. })
|
||||
| canvas::Event::Window(iced::window::Event::Resized(_))
|
||||
| canvas::Event::Mouse(mouse::Event::CursorLeft)
|
||||
| canvas::Event::Window(iced::window::Event::Unfocused) => {
|
||||
state.cancel();
|
||||
state.pointer = None;
|
||||
@@ -843,9 +1229,11 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
canvas::Event::Mouse(
|
||||
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
|
||||
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
|
||||
) => state
|
||||
.pointer
|
||||
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
|
||||
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
|
||||
state.pointer = cursor.position();
|
||||
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
|
||||
cursor
|
||||
}
|
||||
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
@@ -879,7 +1267,7 @@ impl canvas::Program<Message> for Scene {
|
||||
state.press = Some(SequencePress::Message {
|
||||
source,
|
||||
slot,
|
||||
start: cursor.position()?,
|
||||
start: cursor.position()? - self.scroll_offset,
|
||||
first,
|
||||
moved: false,
|
||||
});
|
||||
@@ -889,7 +1277,7 @@ impl canvas::Program<Message> for Scene {
|
||||
state.press = Some(SequencePress::Reorder {
|
||||
occurrence: self.selected,
|
||||
parent: self.rows[&self.selected].0,
|
||||
start: cursor.position()?,
|
||||
start: cursor.position()? - self.scroll_offset,
|
||||
moved: false,
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
@@ -1295,54 +1683,164 @@ mod direct_canvas_tests {
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_scroll_origin_changes_do_not_create_motion_and_unavailable_release_cancels() {
|
||||
fn direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot() {
|
||||
let (f, mut scene) = scene();
|
||||
scene.scroll_offset = iced::Vector::new(35., 140.);
|
||||
let bounds = Rectangle::new(Point::new(20., 230.), scene.size);
|
||||
let slot = scene.slots.last().unwrap().slot;
|
||||
let y = scene.slots.last().unwrap().y;
|
||||
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) - scene.scroll_offset;
|
||||
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) - scene.scroll_offset;
|
||||
let mut state = SequenceInput::default();
|
||||
// Iced Scrollable passes raw event coordinates and a translated cursor.
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: a },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
] {
|
||||
send(&scene, &mut state, event, b + scene.scroll_offset, bounds);
|
||||
}
|
||||
assert!(
|
||||
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b + scene.scroll_offset, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn direct_scroll_resize_and_bounds_changes_cancel_captured_slots() {
|
||||
for change in 0..5 {
|
||||
let (f, scene) = scene();
|
||||
let mut state = SequenceInput::default();
|
||||
let bounds = Rectangle::new(Point::new(20., 70.), scene.size);
|
||||
let a = Point::new(
|
||||
bounds.x + scene.lanes[&f.sender],
|
||||
bounds.y + scene.slots[0].y,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: a },
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
assert!(state.source.is_some());
|
||||
let changed = match change {
|
||||
0 => Rectangle::new(
|
||||
bounds.position() - iced::Vector::new(15., 32.),
|
||||
bounds.size(),
|
||||
),
|
||||
1 => Rectangle::new(bounds.position(), bounds.size() + Size::new(20., 10.)),
|
||||
_ => bounds,
|
||||
};
|
||||
let event = match change {
|
||||
2 => canvas::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: -1. },
|
||||
}),
|
||||
3 => canvas::Event::Window(iced::window::Event::Resized(Size::new(1100., 700.))),
|
||||
_ => canvas::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
)),
|
||||
};
|
||||
let cursor = if change == 4 {
|
||||
mouse::Cursor::Unavailable
|
||||
} else {
|
||||
mouse::Cursor::Available(a)
|
||||
};
|
||||
let event = if change == 4 {
|
||||
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
} else {
|
||||
event
|
||||
};
|
||||
canvas::Program::update(&scene, &mut state, &event, changed, cursor);
|
||||
assert!(
|
||||
state.source.is_none() && state.press.is_none(),
|
||||
"change {change} must cancel the old slot"
|
||||
);
|
||||
let b = Point::new(
|
||||
changed.x + scene.lanes[&f.receiver],
|
||||
changed.y + scene.slots.last().unwrap().y,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
b,
|
||||
changed,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
b,
|
||||
changed,
|
||||
);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
b,
|
||||
changed
|
||||
)
|
||||
.is_none(),
|
||||
"a click after change {change} must start a fresh gesture"
|
||||
);
|
||||
assert_eq!(state.source.unwrap().0, f.receiver);
|
||||
assert_ne!(state.source.unwrap().1.before, Some(f.call));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_bounds_change_during_held_drag_never_commits_old_slot() {
|
||||
let (f, scene) = scene();
|
||||
let mut state = SequenceInput::default();
|
||||
let bounds = Rectangle::new(Point::new(20., 70.), scene.size);
|
||||
let at = Point::new(
|
||||
bounds.x + scene.lanes[&f.sender],
|
||||
bounds.y + scene.slots[0].y,
|
||||
);
|
||||
let bounds = Rectangle::new(Point::ORIGIN, scene.size);
|
||||
let a = Point::new(scene.lanes[&f.sender], scene.slots[0].y);
|
||||
let b = Point::new(scene.lanes[&f.receiver], a.y);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: at },
|
||||
at,
|
||||
mouse::Event::CursorMoved { position: a },
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
at,
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
let shifted = Rectangle::new(
|
||||
bounds.position() - iced::Vector::new(15., 32.),
|
||||
bounds.size(),
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
let shifted = Rectangle::new(Point::new(0., -40.), scene.size);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
at,
|
||||
b,
|
||||
shifted
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
state.source.is_some(),
|
||||
"stationary first click only arms its source"
|
||||
);
|
||||
canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
shifted,
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
assert!(state.source.is_none());
|
||||
assert!(state.source.is_none() && state.press.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn direct_context_escape_unfocus_and_pending_form_cancel_gestures() {
|
||||
@@ -1478,6 +1976,125 @@ mod direct_canvas_tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn direct_alt_execution_stops_each_operand_at_its_reply_and_marks_open_operand() {
|
||||
use super::topology::occurrence;
|
||||
let f = scenario::direct_tests::fixture();
|
||||
let mut p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap();
|
||||
let fragment = occurrence(
|
||||
&p,
|
||||
f.owner,
|
||||
"Alternatives",
|
||||
OccurrenceBody::Fragment {
|
||||
operator: FragmentOperator::Alternative,
|
||||
branches: vec![],
|
||||
},
|
||||
);
|
||||
let fragment_id = fragment.id;
|
||||
let mut commands = vec![Command::CreateElement(fragment)];
|
||||
let mut branches = vec![];
|
||||
let mut replies = vec![];
|
||||
let mut actions = vec![];
|
||||
for index in 0..3 {
|
||||
let mut branch = occurrence(
|
||||
&p,
|
||||
f.owner,
|
||||
&format!("Operand {index}"),
|
||||
OccurrenceBody::Branch {
|
||||
guard: format!("path {index}"),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
branch.parent = Some(fragment_id);
|
||||
let branch_id = branch.id;
|
||||
branches.push(branch_id);
|
||||
commands.push(Command::CreateElement(branch));
|
||||
let mut steps = vec![];
|
||||
if index < 2 {
|
||||
let mut reply = occurrence(
|
||||
&p,
|
||||
f.owner,
|
||||
&format!("Reply {index}"),
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: Some(f.receiver),
|
||||
receiver: Some(f.sender),
|
||||
message_kind: MessageKind::Reply,
|
||||
reply_to: Some(f.call),
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
reply.parent = Some(branch_id);
|
||||
replies.push(reply.id);
|
||||
steps.push(reply.id);
|
||||
commands.push(Command::CreateElement(reply));
|
||||
}
|
||||
let mut action = occurrence(
|
||||
&p,
|
||||
f.owner,
|
||||
&format!("Action {index}"),
|
||||
OccurrenceBody::Action {
|
||||
action: Some(p.relations[&f.exchange].target()),
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
action.parent = Some(branch_id);
|
||||
actions.push(action.id);
|
||||
steps.push(action.id);
|
||||
commands.push(Command::CreateElement(action));
|
||||
commands.push(Command::SetSteps(branch_id, steps));
|
||||
}
|
||||
if let Command::CreateElement(element) = &mut commands[0]
|
||||
&& let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Fragment {
|
||||
branches: stored, ..
|
||||
},
|
||||
..
|
||||
}) = element.shared_mut()
|
||||
{
|
||||
*stored = branches.clone();
|
||||
}
|
||||
p = execute_batch(&p, &commands).unwrap();
|
||||
let scene = Scene::sequence(&p, f.owner, f.call);
|
||||
let bars: Vec<_> = scene
|
||||
.boxes
|
||||
.iter()
|
||||
.filter(|b| b.id == f.call && b.rect.width == 8.)
|
||||
.map(|b| b.rect)
|
||||
.collect();
|
||||
for reply in &replies {
|
||||
let y = scene.rows[reply].1;
|
||||
assert!(
|
||||
bars.iter().any(|r| r.y <= y && r.y + r.height == y),
|
||||
"every reply terminates its own operand segment"
|
||||
);
|
||||
}
|
||||
for action in &actions[..2] {
|
||||
let y = scene.rows[action].1;
|
||||
assert!(
|
||||
!bars.iter().any(|r| r.y <= y && r.y + r.height > y),
|
||||
"a returned operand must not remain active through its following action"
|
||||
);
|
||||
}
|
||||
let last_action = scene.rows[&actions[2]].1;
|
||||
assert!(
|
||||
scene.open_calls.iter().any(|p| p.y > last_action),
|
||||
"the reply-free operand remains explicitly open"
|
||||
);
|
||||
let header = scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|b| b.id == branches[1] && !b.title.is_empty())
|
||||
.unwrap()
|
||||
.rect;
|
||||
assert!(
|
||||
!bars
|
||||
.iter()
|
||||
.any(|r| r.y < header.y + header.height && r.y + r.height > header.y),
|
||||
"segments do not join mutually exclusive operand headers"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call() {
|
||||
let (f, scene) = scene();
|
||||
let bar = scene
|
||||
|
||||
Reference in New Issue
Block a user