Add direct scenario gestures and atomic ordered message insertion
This commit is contained in:
@@ -52,7 +52,7 @@ pub struct State {
|
||||
pub active_owner: Option<Id>,
|
||||
pub diagram: state_view::State,
|
||||
topology: topology::State,
|
||||
scenario: scenario::State,
|
||||
pub(crate) scenario: scenario::State,
|
||||
states: states::State,
|
||||
}
|
||||
const PENDING_DRAFT: &str = "Apply or cancel the current behavior form before starting another.";
|
||||
@@ -254,6 +254,46 @@ struct Scene {
|
||||
size: Size,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
slots: Vec<SequenceSlot>,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
open_calls: Vec<Point>,
|
||||
}
|
||||
struct SequenceSlot {
|
||||
slot: scenario::Slot,
|
||||
y: f32,
|
||||
label: String,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct SequenceInput {
|
||||
context: Option<scenario::Context>,
|
||||
tool: scenario::CanvasTool,
|
||||
pointer: Option<Point>,
|
||||
source: Option<(Id, scenario::Slot)>,
|
||||
press: Option<SequencePress>,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
enum SequencePress {
|
||||
Message {
|
||||
source: Id,
|
||||
slot: scenario::Slot,
|
||||
start: Point,
|
||||
first: bool,
|
||||
moved: bool,
|
||||
},
|
||||
Reorder {
|
||||
occurrence: Id,
|
||||
parent: Id,
|
||||
start: Point,
|
||||
moved: bool,
|
||||
},
|
||||
}
|
||||
impl SequenceInput {
|
||||
fn cancel(&mut self) {
|
||||
self.source = None;
|
||||
self.press = None;
|
||||
}
|
||||
}
|
||||
impl Scene {
|
||||
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
||||
@@ -263,6 +303,11 @@ impl Scene {
|
||||
lines: vec![],
|
||||
size: Size::new(700., 200.),
|
||||
selected,
|
||||
interaction: None,
|
||||
lanes: BTreeMap::new(),
|
||||
slots: vec![],
|
||||
rows: BTreeMap::new(),
|
||||
open_calls: vec![],
|
||||
};
|
||||
let mut lanes = p
|
||||
.children(owner)
|
||||
@@ -333,15 +378,69 @@ impl Scene {
|
||||
.map(|(i, (id, ..))| (*id, 160. + i as f32 * 240.))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
scene.size.width = (lanes.len() as f32 * 240. + 80.).max(680.);
|
||||
let mut y = 120.;
|
||||
scene.occurrences(
|
||||
p,
|
||||
p.elements[&owner].steps().map(Vec::as_slice).unwrap_or(&[]),
|
||||
scenario,
|
||||
&lane_x,
|
||||
&mut y,
|
||||
0,
|
||||
);
|
||||
if scenario {
|
||||
scene.lanes = lane_x
|
||||
.iter()
|
||||
.filter(|(id, _)| {
|
||||
matches!(
|
||||
p.elements.get(id).and_then(|e| e.shared()),
|
||||
Some(SharedBody::Lifeline {
|
||||
represented: Some(_)
|
||||
})
|
||||
)
|
||||
})
|
||||
.map(|(id, x)| (*id, *x))
|
||||
.collect();
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
for (id, name, detail) in lanes {
|
||||
let x = lane_x[&id];
|
||||
@@ -368,12 +467,30 @@ impl Scene {
|
||||
fn occurrences(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
steps: &[Id],
|
||||
owner: Id,
|
||||
scenario: bool,
|
||||
lanes: &BTreeMap<Id, f32>,
|
||||
y: &mut f32,
|
||||
depth: usize,
|
||||
) {
|
||||
let steps = p
|
||||
.elements
|
||||
.get(&owner)
|
||||
.and_then(|e| {
|
||||
e.steps().map(Vec::as_slice).or_else(|| {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = e.shared()
|
||||
{
|
||||
Some(branches.as_slice())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(&[]);
|
||||
let ordered = p.elements.get(&owner).and_then(|e| e.steps()).is_some();
|
||||
for (index, id) in steps.iter().enumerate() {
|
||||
let Some(e) = p.elements.get(id) else {
|
||||
continue;
|
||||
@@ -381,11 +498,23 @@ impl Scene {
|
||||
let Some(SharedBody::Occurrence { body, .. }) = e.shared() else {
|
||||
continue;
|
||||
};
|
||||
if ordered {
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
before: Some(*id),
|
||||
};
|
||||
self.slots.push(SequenceSlot {
|
||||
slot,
|
||||
y: *y - 42.,
|
||||
label: scenario::slot_label(p, slot),
|
||||
});
|
||||
self.rows.insert(*id, (owner, *y));
|
||||
}
|
||||
match body {
|
||||
OccurrenceBody::Fragment { operator, branches } => {
|
||||
OccurrenceBody::Fragment { operator, .. } => {
|
||||
let top = *y;
|
||||
*y += 55.;
|
||||
self.occurrences(p, branches, scenario, lanes, y, depth + 1);
|
||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1);
|
||||
let rect = Rectangle::new(
|
||||
Point::new(18. + depth as f32 * 10., top),
|
||||
Size::new(
|
||||
@@ -403,9 +532,9 @@ impl Scene {
|
||||
marker: Some(Kind::Occurrence),
|
||||
},
|
||||
);
|
||||
*y += 20.;
|
||||
*y += if scenario { 64. } else { 20. };
|
||||
}
|
||||
OccurrenceBody::Branch { guard, occurrences } => {
|
||||
OccurrenceBody::Branch { guard, .. } => {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
rect: Rectangle::new(
|
||||
@@ -423,8 +552,8 @@ impl Scene {
|
||||
detail: String::new(),
|
||||
marker: Some(Kind::Occurrence),
|
||||
});
|
||||
*y += 62.;
|
||||
self.occurrences(p, occurrences, scenario, lanes, y, depth + 1);
|
||||
*y += if scenario { 90. } else { 62. };
|
||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1);
|
||||
}
|
||||
OccurrenceBody::Exchange {
|
||||
exchange,
|
||||
@@ -487,15 +616,6 @@ impl Scene {
|
||||
label_at: Point::new(a.min(*b) + 12., *y - 26.),
|
||||
dashed: matches!(message_kind, MessageKind::Reply),
|
||||
});
|
||||
if scenario && matches!(message_kind, MessageKind::Call) {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
rect: Rectangle::new(Point::new(*b - 4., *y), Size::new(8., 80.)),
|
||||
title: String::new(),
|
||||
detail: String::new(),
|
||||
marker: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
@@ -575,51 +695,291 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ordered {
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
before: None,
|
||||
};
|
||||
self.slots.push(SequenceSlot {
|
||||
slot,
|
||||
y: *y - 42.,
|
||||
label: scenario::slot_label(p, slot),
|
||||
});
|
||||
}
|
||||
}
|
||||
fn slot_at(&self, point: Point, parent: Option<Id>) -> Option<&SequenceSlot> {
|
||||
self.slots
|
||||
.iter()
|
||||
.min_by(|a, b| (a.y - point.y).abs().total_cmp(&(b.y - point.y).abs()))
|
||||
.filter(|slot| parent.is_none_or(|id| id == slot.slot.parent))
|
||||
}
|
||||
fn lane_at(&self, point: Point) -> Option<Id> {
|
||||
if let Some(header) = self
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|b| self.lanes.contains_key(&b.id) && b.rect.contains(point))
|
||||
{
|
||||
return Some(header.id);
|
||||
}
|
||||
self.lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| (**x - point.x).abs() < 12. / self.scale)
|
||||
.min_by(|a, b| (a.1 - point.x).abs().total_cmp(&(b.1 - point.x).abs()))
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
fn reorder_handle(&self) -> Option<Rectangle> {
|
||||
let (_, y) = self.rows.get(&self.selected)?;
|
||||
Some(Rectangle::new(
|
||||
Point::new(6., *y - 12.),
|
||||
Size::new(22., 24.),
|
||||
))
|
||||
}
|
||||
fn hit(&self, point: Point) -> Option<Id> {
|
||||
self.boxes
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|b| {
|
||||
!b.title.is_empty()
|
||||
&& b.rect.contains(point)
|
||||
&& (b.marker != Some(Kind::Occurrence) || point.y < b.rect.y + 38.)
|
||||
})
|
||||
.map(|b| b.id)
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
.iter()
|
||||
.find(|line| {
|
||||
!line.label.is_empty()
|
||||
&& (Rectangle::new(
|
||||
line.label_at,
|
||||
Size::new(
|
||||
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
|
||||
25.,
|
||||
),
|
||||
)
|
||||
.contains(point)
|
||||
|| line.points.windows(2).any(|p| {
|
||||
crate::diagram::distance_to_segment(point, p[0], p[1])
|
||||
< 8. / self.scale
|
||||
}))
|
||||
})
|
||||
.map(|line| line.id)
|
||||
})
|
||||
.or_else(|| {
|
||||
self.boxes
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|b| b.rect.contains(point) && b.title.is_empty())
|
||||
.map(|b| b.id)
|
||||
})
|
||||
.or_else(|| self.lane_at(point))
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
.iter()
|
||||
.find(|line| {
|
||||
line.label.is_empty()
|
||||
&& line.points.windows(2).any(|p| {
|
||||
crate::diagram::distance_to_segment(point, p[0], p[1])
|
||||
< 8. / self.scale
|
||||
})
|
||||
})
|
||||
.map(|line| line.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
impl canvas::Program<Message> for Scene {
|
||||
type State = ();
|
||||
type State = SequenceInput;
|
||||
fn update(
|
||||
&self,
|
||||
_: &mut (),
|
||||
state: &mut SequenceInput,
|
||||
event: &canvas::Event,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Option<Action<Message>> {
|
||||
if matches!(
|
||||
event,
|
||||
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
) && let Some(p) = cursor.position_in(bounds)
|
||||
use scenario::{CanvasEvent, CanvasTool};
|
||||
let (context, tool, editable) = self
|
||||
.interaction
|
||||
.map_or((None, CanvasTool::Select, false), |(c, t, e)| {
|
||||
(Some(c), t, e)
|
||||
});
|
||||
if state.context != context || state.tool != tool {
|
||||
state.cancel();
|
||||
state.pointer = None;
|
||||
state.context = context;
|
||||
state.tool = tool;
|
||||
}
|
||||
if !editable {
|
||||
state.cancel();
|
||||
}
|
||||
// Scrollable marks a pointer outside its visible viewport unavailable,
|
||||
// even when it lies inside the full (scrolled) canvas rectangle.
|
||||
let visible = cursor.position().is_some();
|
||||
if !visible
|
||||
&& matches!(
|
||||
event,
|
||||
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
)
|
||||
{
|
||||
let p = Point::new(p.x / self.scale, p.y / self.scale);
|
||||
if let Some(b) = self.boxes.iter().rev().find(|b| {
|
||||
b.rect.contains(p) && (b.marker != Some(Kind::Occurrence) || p.y < b.rect.y + 38.)
|
||||
}) {
|
||||
return Some(Action::publish(Message::Select(b.id)).and_capture());
|
||||
state.cancel();
|
||||
return Some(Action::request_redraw());
|
||||
}
|
||||
let cursor = match event {
|
||||
canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
||||
state.pointer = Some(*position);
|
||||
if let Some(press) = &mut state.press {
|
||||
match press {
|
||||
SequencePress::Message { start, moved, .. }
|
||||
| SequencePress::Reorder { start, moved, .. } => {
|
||||
*moved |= position.distance(*start) >= 3.
|
||||
}
|
||||
}
|
||||
}
|
||||
mouse::Cursor::Available(*position)
|
||||
}
|
||||
if let Some(l) = self.lines.iter().find(|l| {
|
||||
!l.label.is_empty()
|
||||
&& (Rectangle::new(
|
||||
l.label_at,
|
||||
Size::new((l.label.chars().count() as f32 * 6.8 + 8.).min(225.), 25.),
|
||||
)
|
||||
.contains(p)
|
||||
|| l.points
|
||||
.windows(2)
|
||||
.any(|v| crate::diagram::distance_to_segment(p, v[0], v[1]) < 8.))
|
||||
}) {
|
||||
return Some(Action::publish(Message::Select(l.id)).and_capture());
|
||||
canvas::Event::Mouse(mouse::Event::CursorLeft)
|
||||
| canvas::Event::Window(iced::window::Event::Unfocused) => {
|
||||
state.cancel();
|
||||
state.pointer = None;
|
||||
return Some(Action::request_redraw());
|
||||
}
|
||||
canvas::Event::Mouse(
|
||||
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
|
||||
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
|
||||
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
|
||||
state.pointer = cursor.position();
|
||||
cursor
|
||||
}
|
||||
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
|
||||
..
|
||||
}) if state.source.is_some() || state.press.is_some() => {
|
||||
state.cancel();
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
_ => cursor,
|
||||
};
|
||||
let world = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
match event {
|
||||
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if !visible {
|
||||
return None;
|
||||
}
|
||||
let point = world?;
|
||||
if let Some(context) = context {
|
||||
if !editable {
|
||||
return None;
|
||||
}
|
||||
if let CanvasTool::Message(_) = tool {
|
||||
let first = state.source.is_none();
|
||||
let (source, slot) = state.source.or_else(|| {
|
||||
Some((self.lane_at(point)?, self.slot_at(point, None)?.slot))
|
||||
})?;
|
||||
state.source = Some((source, slot));
|
||||
state.press = Some(SequencePress::Message {
|
||||
source,
|
||||
slot,
|
||||
start: cursor.position()?,
|
||||
first,
|
||||
moved: false,
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if self.reorder_handle().is_some_and(|r| r.contains(point)) {
|
||||
state.press = Some(SequencePress::Reorder {
|
||||
occurrence: self.selected,
|
||||
parent: self.rows[&self.selected].0,
|
||||
start: cursor.position()?,
|
||||
moved: false,
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
return self.hit(point).map(|id| {
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(context, id)))
|
||||
.and_capture()
|
||||
});
|
||||
}
|
||||
self.hit(point)
|
||||
.map(|id| Action::publish(Message::Select(id)).and_capture())
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||
let press = state.press.take()?;
|
||||
let context = context?;
|
||||
match press {
|
||||
SequencePress::Message { source, slot, .. }
|
||||
if tool == CanvasTool::Message(scenario::MessageStyle::Timer)
|
||||
&& world.is_some() =>
|
||||
{
|
||||
state.cancel();
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Begin {
|
||||
context,
|
||||
slot,
|
||||
sender: source,
|
||||
receiver: source,
|
||||
style: scenario::MessageStyle::Timer,
|
||||
}))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
SequencePress::Message {
|
||||
source,
|
||||
slot,
|
||||
first,
|
||||
moved,
|
||||
..
|
||||
} if moved || !first => {
|
||||
if let Some(receiver) = world.and_then(|p| self.lane_at(p)) {
|
||||
let CanvasTool::Message(style) = tool else {
|
||||
return None;
|
||||
};
|
||||
state.cancel();
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Begin {
|
||||
context,
|
||||
slot,
|
||||
sender: source,
|
||||
receiver,
|
||||
style,
|
||||
}))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
}
|
||||
SequencePress::Reorder {
|
||||
occurrence,
|
||||
parent,
|
||||
moved: true,
|
||||
..
|
||||
} => {
|
||||
if let Some(slot) = world.and_then(|p| self.slot_at(p, Some(parent))) {
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Reorder {
|
||||
context,
|
||||
occurrence,
|
||||
slot: slot.slot,
|
||||
}))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(Action::request_redraw().and_capture())
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) if context.is_some() => {
|
||||
Some(Action::request_redraw())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
_: &(),
|
||||
state: &SequenceInput,
|
||||
renderer: &Renderer,
|
||||
_: &Theme,
|
||||
bounds: Rectangle,
|
||||
_: mouse::Cursor,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
@@ -723,11 +1083,89 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
}
|
||||
}
|
||||
for point in &self.open_calls {
|
||||
f.fill_text(Text {
|
||||
content: "…".into(),
|
||||
position: *point,
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if let Some((context, tool, true)) = self.interaction {
|
||||
let active = state.context == Some(context) && state.tool == tool;
|
||||
let point = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
let source = active.then_some(state.source).flatten();
|
||||
let reorder = active.then_some(state.press).flatten().and_then(|press| {
|
||||
if let SequencePress::Reorder { parent, .. } = press {
|
||||
Some(parent)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if tool == scenario::CanvasTool::Select
|
||||
&& let Some(handle) = self.reorder_handle()
|
||||
{
|
||||
f.fill_rectangle(handle.position(), handle.size(), style::TEAL);
|
||||
f.fill_text(Text {
|
||||
content: "↕".into(),
|
||||
position: handle.position() + iced::Vector::new(5., 2.),
|
||||
size: 16.into(),
|
||||
color: Color::WHITE,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let slot = if let Some((_, slot)) = source {
|
||||
self.slots.iter().find(|s| s.slot == slot)
|
||||
} else if matches!(tool, scenario::CanvasTool::Message(_)) || reorder.is_some() {
|
||||
point.and_then(|p| self.slot_at(p, reorder))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(slot) = slot {
|
||||
f.stroke(
|
||||
&Path::line(
|
||||
Point::new(32., slot.y),
|
||||
Point::new(self.size.width - 24., slot.y),
|
||||
),
|
||||
Stroke::default().with_color(style::TEAL).with_width(1.5),
|
||||
);
|
||||
f.fill_rectangle(
|
||||
Point::new(34., slot.y - 20.),
|
||||
Size::new((self.size.width - 70.).min(650.), 19.),
|
||||
Color::WHITE,
|
||||
);
|
||||
f.fill_text(Text {
|
||||
content: short(&format!("Insert: {}", slot.label), 88),
|
||||
position: Point::new(38., slot.y - 19.),
|
||||
size: 11.into(),
|
||||
color: style::TEAL,
|
||||
..Default::default()
|
||||
});
|
||||
if let Some((source, _)) = source
|
||||
&& let Some(x) = self.lanes.get(&source)
|
||||
&& let Some(point) = point
|
||||
{
|
||||
let to = self
|
||||
.lane_at(point)
|
||||
.and_then(|id| self.lanes.get(&id))
|
||||
.copied()
|
||||
.unwrap_or(point.x);
|
||||
f.stroke(
|
||||
&Path::line(Point::new(*x, slot.y), Point::new(to, slot.y)),
|
||||
Stroke::default().with_color(style::TEAL).with_width(3.),
|
||||
);
|
||||
f.fill(&Path::circle(Point::new(to, slot.y), 5.), style::TEAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
vec![f.into_geometry()]
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_: &(),
|
||||
_: &SequenceInput,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> mouse::Interaction {
|
||||
@@ -739,6 +1177,332 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod direct_canvas_tests {
|
||||
use super::*;
|
||||
use scenario::{CanvasEvent, CanvasTool, MessageStyle};
|
||||
fn scene() -> (scenario::direct_tests::Fixture, Scene) {
|
||||
let f = scenario::direct_tests::fixture();
|
||||
let mut scene = Scene::sequence(&f.project, f.owner, f.call);
|
||||
scene.interaction = Some((
|
||||
scenario::direct_tests::context(f.owner),
|
||||
CanvasTool::Message(MessageStyle::CallReply),
|
||||
true,
|
||||
));
|
||||
(f, scene)
|
||||
}
|
||||
fn send(
|
||||
scene: &Scene,
|
||||
state: &mut SequenceInput,
|
||||
event: mouse::Event,
|
||||
final_cursor: Point,
|
||||
bounds: Rectangle,
|
||||
) -> Option<CanvasEvent> {
|
||||
canvas::Program::update(
|
||||
scene,
|
||||
state,
|
||||
&canvas::Event::Mouse(event),
|
||||
bounds,
|
||||
mouse::Cursor::Available(final_cursor),
|
||||
)
|
||||
.and_then(|action| action.into_inner().0)
|
||||
.and_then(|message| {
|
||||
if let Message::ScenarioCanvas(event) = message {
|
||||
Some(event)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn direct_drag_and_click_click_use_ordered_sources_and_publish_once() {
|
||||
for drag in [false, true] {
|
||||
let (f, scene) = scene();
|
||||
let mut state = SequenceInput::default();
|
||||
let bounds = Rectangle::new(Point::new(30., 50.), scene.size);
|
||||
let a = Point::new(
|
||||
bounds.x + scene.lanes[&f.sender],
|
||||
bounds.y + scene.slots[0].y,
|
||||
);
|
||||
let b = Point::new(bounds.x + scene.lanes[&f.receiver], a.y);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: a },
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
b,
|
||||
bounds
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
if !drag {
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
b,
|
||||
bounds
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(state.source.unwrap().0, f.sender);
|
||||
}
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
if !drag {
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
}
|
||||
let result = send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
matches!(result, Some(CanvasEvent::Begin { sender, receiver, slot, .. }) if sender == f.sender && receiver == f.receiver && slot.before == Some(f.call))
|
||||
);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
b,
|
||||
bounds
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(state.source.is_none() && state.press.is_none());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_scroll_origin_changes_do_not_create_motion_and_unavailable_release_cancels() {
|
||||
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,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: at },
|
||||
at,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
at,
|
||||
bounds,
|
||||
);
|
||||
let shifted = Rectangle::new(
|
||||
bounds.position() - iced::Vector::new(15., 32.),
|
||||
bounds.size(),
|
||||
);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
at,
|
||||
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());
|
||||
}
|
||||
#[test]
|
||||
fn direct_context_escape_unfocus_and_pending_form_cancel_gestures() {
|
||||
for cancellation in 0..5 {
|
||||
let (f, mut scene) = scene();
|
||||
let mut state = SequenceInput::default();
|
||||
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: a },
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
match cancellation {
|
||||
0 => {
|
||||
scene.interaction.as_mut().unwrap().0.revision += 1;
|
||||
}
|
||||
1 => {
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Select;
|
||||
}
|
||||
2 => {
|
||||
scene.interaction.as_mut().unwrap().2 = false;
|
||||
}
|
||||
3 => {
|
||||
canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&canvas::Event::Window(iced::window::Event::Unfocused),
|
||||
bounds,
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
|
||||
modified_key: iced::keyboard::Key::Named(
|
||||
iced::keyboard::key::Named::Escape,
|
||||
),
|
||||
physical_key: iced::keyboard::key::Physical::Code(
|
||||
iced::keyboard::key::Code::Escape,
|
||||
),
|
||||
location: iced::keyboard::Location::Standard,
|
||||
modifiers: iced::keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
}),
|
||||
bounds,
|
||||
mouse::Cursor::Available(b),
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
b,
|
||||
bounds
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(state.press.is_none() && state.source.is_none());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_selected_handle_reorders_and_hits_keep_semantic_identity() {
|
||||
let (f, mut scene) = scene();
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Select;
|
||||
let bounds = Rectangle::new(Point::ORIGIN, scene.size);
|
||||
let header = scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|b| b.id == f.sender)
|
||||
.unwrap()
|
||||
.rect
|
||||
.center();
|
||||
assert_eq!(scene.hit(header), Some(f.sender));
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.sender], scene.size.height - 28.)),
|
||||
Some(f.sender)
|
||||
);
|
||||
let line = scene.lines.iter().find(|line| line.id == f.reply).unwrap();
|
||||
assert_eq!(
|
||||
scene.hit(line.label_at + iced::Vector::new(4., 4.)),
|
||||
Some(f.reply)
|
||||
);
|
||||
let mut state = SequenceInput::default();
|
||||
let start = scene.reorder_handle().unwrap().center();
|
||||
let end = Point::new(start.x, scene.slots.last().unwrap().y);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
start,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
start,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
end,
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), end, bounds), Some(CanvasEvent::Reorder { occurrence, slot, .. }) if occurrence == f.call && slot.parent == f.owner && slot.before.is_none())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call() {
|
||||
let (f, scene) = scene();
|
||||
let bar = scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|b| b.id == f.call && b.rect.width == 8.)
|
||||
.unwrap();
|
||||
assert_eq!(bar.rect.y + bar.rect.height, scene.rows[&f.reply].1);
|
||||
assert!(scene.open_calls.is_empty());
|
||||
let p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap();
|
||||
let scene = Scene::sequence(&p, f.owner, f.call);
|
||||
assert_eq!(scene.open_calls.len(), 1);
|
||||
assert_eq!(
|
||||
scene
|
||||
.boxes
|
||||
.iter()
|
||||
.find(|b| b.id == f.call && b.rect.width == 8.)
|
||||
.unwrap()
|
||||
.rect
|
||||
.height,
|
||||
26.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user