Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
Codex
2026-09-10 10:06:55 -07:00
parent 4b6b5fc197
commit e2ecfea69b
162 changed files with 17595 additions and 926 deletions
+518 -163
View File
@@ -3,6 +3,7 @@
use super::*;
use iced::widget::{Column, text_input};
use std::collections::BTreeSet;
pub(crate) mod contextual;
mod direct;
mod view;
use direct::Input;
@@ -20,6 +21,7 @@ pub enum Tool {
Control,
Connect,
Branch,
Contextual,
}
impl Tool {
fn title(self) -> &'static str {
@@ -30,6 +32,7 @@ impl Tool {
Self::Control => "Add control",
Self::Connect => "Connect contributions",
Self::Branch => "Insert two paths",
Self::Contextual => "Connect qualified contributions",
}
}
}
@@ -50,6 +53,13 @@ pub enum Event {
Control(ControlChoice),
Search(usize, String),
Page(usize, usize),
Contextual(contextual::Event),
Shelf(Shelf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shelf {
Tools,
Selection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ControlChoice(pub ControlKind);
@@ -70,11 +80,15 @@ pub struct State {
tool: CanvasTool,
expanded: BTreeSet<Id>,
scroll_offsets: [iced::Vector; 2],
return_to: Vec<(Id, Id)>,
return_to: Vec<(Id, Id, Option<oa_core::topology_context::ContextAddress>)>,
draft: Option<Draft>,
viewport: Option<Size>,
zoom: Option<f32>,
fit: bool,
pub(crate) focused: bool,
contextual_expanded: BTreeSet<OccurrenceEndpoint>,
selected_context: Option<oa_core::topology_context::ContextAddress>,
shelf: Option<Shelf>,
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
@@ -99,6 +113,8 @@ struct Draft {
guard: String,
alternate_guard: String,
control: ControlKind,
#[serde(default)]
contextual: Option<contextual::Draft>,
}
impl State {
pub(super) fn recovery_snapshot(&self) -> Result<Option<serde_json::Value>, String> {
@@ -140,6 +156,9 @@ impl State {
return Err(PENDING_DRAFT.into());
}
match event {
Event::Shelf(shelf) => {
self.shelf = (self.shelf != Some(shelf)).then_some(shelf);
}
Event::Start(owner, tool, selected) => {
let e = p
.elements
@@ -178,7 +197,17 @@ impl State {
guard: String::new(),
alternate_guard: "else".into(),
control: ControlKind::Choice,
contextual: (tool == Tool::Contextual)
.then(|| contextual::Draft::new(p, owner, selected)),
});
if let Some(endpoint) = self
.qualified_selection(p, owner)
.map(|address| address.endpoint.clone())
&& let Some(contextual) =
self.draft.as_mut().and_then(|d| d.contextual.as_mut())
{
contextual.seed_source(endpoint);
}
}
Event::Cancel => self.acknowledge_commit(),
Event::Submit => return self.prepare_pending(p).map(Some),
@@ -200,6 +229,11 @@ impl State {
d.browsers[index].page = 0;
}
Event::Page(index, value) if index < 4 => d.browsers[index].page = value,
Event::Contextual(event) => {
if let Some(contextual) = &mut d.contextual {
contextual.update(event);
}
}
_ => {}
}
}
@@ -212,11 +246,31 @@ impl State {
if !self.fit {
return self.zoom.unwrap_or(1.);
}
let width = self.viewport.map_or(fallback_width, |s| s.width);
((width - 24.).max(1.) / graph.width.max(1.))
.min(450. / graph.height.max(1.))
let available = self.fit_area(fallback_width);
(available.width / graph.width.max(1.))
.min(available.height / graph.height.max(1.))
.clamp(0.05, 1.)
}
fn fit_area(&self, fallback_width: f32) -> Size {
// The sensor measures the inner scroller, independent of graph content.
// Leave a small gutter for scrollbars; before first layout use a compact
// provisional viewport and recompute when the sensor reports actual size.
let viewport = self.viewport.unwrap_or(Size::new(fallback_width, 180.));
Size::new(
(viewport.width - 24.).max(1.),
(viewport.height - 10.).max(1.),
)
}
fn canvas_offset(&self) -> iced::Vector {
self.scroll_offsets[0]
}
fn fit_limited(&self, graph: Size, fallback_width: f32) -> bool {
let available = self.fit_area(fallback_width);
self.fit && (graph.width * 0.05 > available.width || graph.height * 0.05 > available.height)
}
}
mod recovery_insertion {
@@ -461,6 +515,12 @@ impl Draft {
if !matches!(owner.kind(), Kind::Process | Kind::FunctionalChain) {
return Err("Choose a process or chain.".into());
}
if (self.tool == Tool::Contextual) != self.contextual.is_some() {
return Err("The qualified link draft and selected tool do not agree.".into());
}
if let Some(draft) = &self.contextual {
return draft.command(p, self.owner, &self.name);
}
for id in [self.before, self.after].into_iter().flatten() {
if !is_contribution(p, self.owner, id) {
return Err("Choose contributions from this process or chain.".into());
@@ -638,6 +698,7 @@ impl Draft {
lifeline: None,
},
Tool::Exchange => OccurrenceBody::Exchange {
operation: None,
exchange: target,
sender: None,
receiver: None,
@@ -744,10 +805,27 @@ struct GraphNode {
detail: String,
control: Option<ControlKind>,
source: Option<Id>,
preview: Vec<(Id, String)>,
preview_links: Vec<(Id, Id)>,
preview: Vec<(OccurrenceEndpoint, String)>,
preview_links: Vec<(OccurrenceEndpoint, OccurrenceEndpoint)>,
preview_total: usize,
}
impl GraphNode {
fn endpoint_rect(&self, endpoint: &OccurrenceEndpoint) -> Rectangle {
self.preview
.iter()
.position(|(candidate, _)| candidate == endpoint)
.map(|i| {
Rectangle::new(
Point::new(
self.rect.x + 12. + (i % 2) as f32 * 125.,
self.rect.y + 118. + (i / 2) as f32 * 44.,
),
Size::new(113., 32.),
)
})
.unwrap_or(self.rect)
}
}
struct GraphEdge {
id: Id,
source: Id,
@@ -755,7 +833,30 @@ struct GraphEdge {
guard: String,
points: Vec<Point>,
label: Rectangle,
incidence: bool,
layer: EdgeLayer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EdgeLayer {
Direct,
Contextual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PaintPass {
DirectEdges,
NodeBackgrounds,
ContextualEdges,
NodeContents,
Labels,
}
const PAINT_PASSES: [PaintPass; 5] = [
PaintPass::DirectEdges,
PaintPass::NodeBackgrounds,
PaintPass::ContextualEdges,
PaintPass::NodeContents,
PaintPass::Labels,
];
mod contextual_routing;
struct Graph<'a> {
project: &'a Project,
interaction: Option<(Context, CanvasTool, bool)>,
@@ -771,6 +872,16 @@ impl<'a> Graph<'a> {
Self::expanded(p, owner, selected, &BTreeSet::new())
}
fn expanded(p: &'a Project, owner: Id, selected: Id, expanded: &BTreeSet<Id>) -> Self {
Self::expanded_qualified(p, owner, selected, expanded, &BTreeSet::new())
}
fn expanded_qualified(
p: &'a Project,
owner: Id,
selected: Id,
expanded: &BTreeSet<Id>,
expanded_paths: &BTreeSet<OccurrenceEndpoint>,
) -> Self {
let context_index = oa_core::topology_context::TopologyIndex::new(p);
let choices = contributions(p, owner);
let links = sequence_links(p, owner);
let ids: BTreeSet<_> = choices.iter().map(|c| c.id).collect();
@@ -864,26 +975,51 @@ impl<'a> Graph<'a> {
if let Some(source) = sequence {
let contributions = contributions(p, *source);
node.preview_total = contributions.len();
node.preview = contributions
.into_iter()
.take(if expanded.contains(&e.id) {
usize::MAX
} else {
4
})
.map(|c| (c.id, p.named(c.id).to_owned()))
.collect();
node.preview = contextual::preview(
&context_index,
p,
owner,
e.id,
expanded_paths,
if expanded.contains(&e.id) { 512 } else { 4 },
)
.into_iter()
.map(|endpoint| {
let name = format!(
"{}{}",
"› ".repeat(endpoint.references.len().saturating_sub(1).min(3)),
p.named(endpoint.occurrence)
);
(endpoint, name)
})
.collect();
node.rect.height =
(132. + node.preview.len().div_ceil(2) as f32 * 44.).max(218.);
let preview_ids: BTreeSet<_> =
node.preview.iter().map(|(id, _)| *id).collect();
node.preview.iter().map(|(id, _)| id.clone()).collect();
node.preview_links = sequence_links(p, *source)
.into_iter()
.filter(|r| {
preview_ids.contains(&r.source())
&& preview_ids.contains(&r.target())
preview_ids.contains(&OccurrenceEndpoint {
references: vec![e.id],
occurrence: r.source(),
}) && preview_ids.contains(&OccurrenceEndpoint {
references: vec![e.id],
occurrence: r.target(),
})
})
.map(|r| {
(
OccurrenceEndpoint {
references: vec![e.id],
occurrence: r.source(),
},
OccurrenceEndpoint {
references: vec![e.id],
occurrence: r.target(),
},
)
})
.map(|r| (r.source(), r.target()))
.collect();
}
}
@@ -944,11 +1080,83 @@ impl<'a> Graph<'a> {
guard,
points,
label,
incidence: false,
layer: EdgeLayer::Direct,
});
}
let mut contextual_pairs = BTreeSet::new();
for r in p.relations.values() {
let RelationBody::ContextualLink {
owner: caller,
source,
target,
purpose,
} = &r.body
else {
continue;
};
if *caller != owner {
continue;
}
let source_id = source
.references
.first()
.copied()
.unwrap_or(source.occurrence);
let target_id = target
.references
.first()
.copied()
.unwrap_or(target.occurrence);
let Some(a) = nodes
.iter()
.find(|n| n.id == source_id)
.map(|n| contextual_routing::anchor(n, source, false))
else {
continue;
};
let Some(b) = nodes
.iter()
.find(|n| n.id == target_id)
.map(|n| contextual_routing::anchor(n, target, true))
else {
continue;
};
let below = size.height + 30. + edges.len() as f32 * 48.;
let first = contextual_pairs.insert((source.clone(), target.clone()));
let (points, label) = contextual_routing::route(a, b, below, first);
let (caption, incidence) = match purpose {
ContextualLinkPurpose::Precedence { guard } => {
(format!("Precedence · {guard}"), false)
}
ContextualLinkPurpose::Exchange { exchange } => {
(format!("Assembly · {}", p.named(*exchange)), true)
}
ContextualLinkPurpose::CommonWork { work } => {
(format!("Common work · {}", p.named(*work)), true)
}
};
edges.push(GraphEdge {
id: r.id,
source: source_id,
target: target_id,
guard: caption,
points,
label,
incidence,
layer: EdgeLayer::Contextual,
});
}
for e in &edges {
for pt in &e.points {
size.height = size.height.max(pt.y + 30.);
if e.layer == EdgeLayer::Contextual {
size.width = size.width.max(pt.x + 24.);
}
}
if e.layer == EdgeLayer::Contextual {
size.width = size.width.max(e.label.x + e.label.width + 24.);
size.height = size.height.max(e.label.y + e.label.height + 24.);
}
}
Self {
@@ -1053,155 +1261,196 @@ impl canvas::Program<Message> for Graph<'_> {
let mut f = Frame::new(renderer, bounds.size());
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
f.scale(self.scale);
for e in &self.edges {
let selected =
e.id == self.selected || e.source == self.selected || e.target == self.selected;
draw_arrow(
&mut f,
&e.points,
if selected { style::TEAL } else { style::MUTED },
if selected { 2.5 } else { 1.5 },
);
}
for n in &self.nodes {
let rect = Path::rounded_rectangle(n.rect.position(), n.rect.size(), 10.into());
f.fill(&rect, Color::from_rgb8(243, 248, 248));
f.stroke(
&rect,
Stroke::default()
.with_color(if n.id == self.selected {
style::TEAL
} else {
style::LINE
})
.with_width(if n.id == self.selected { 2.5 } else { 1. }),
);
let text_x = n.rect.x + if n.control.is_some() { 68. } else { 12. };
if let Some(kind) = n.control {
let center = Point::new(n.rect.x + 33., n.rect.y + 34.);
let glyph = match kind {
ControlKind::Choice | ControlKind::Merge => Path::new(|p| {
p.move_to(Point::new(center.x, center.y - 22.));
p.line_to(Point::new(center.x + 23., center.y));
p.line_to(Point::new(center.x, center.y + 22.));
p.line_to(Point::new(center.x - 23., center.y));
p.close();
}),
ControlKind::Fork | ControlKind::Join => Path::rectangle(
Point::new(center.x - 23., center.y - 5.),
Size::new(46., 10.),
),
ControlKind::Loop => Path::circle(center, 23.),
};
f.stroke(
&glyph,
Stroke::default().with_color(style::TEAL).with_width(2.),
);
if matches!(kind, ControlKind::Fork | ControlKind::Join) {
f.fill(&glyph, style::TEAL);
} else {
draw_text(
&mut f,
if kind == ControlKind::Loop {
"IT"
for pass in PAINT_PASSES {
match pass {
PaintPass::DirectEdges | PaintPass::ContextualEdges => {
for e in self.edges.iter().filter(|edge| {
edge.layer
== if pass == PaintPass::DirectEdges {
EdgeLayer::Direct
} else {
EdgeLayer::Contextual
}
}) {
let selected = e.id == self.selected
|| e.source == self.selected
|| e.target == self.selected;
if e.incidence {
f.stroke(
&Path::new(|path| {
path.move_to(e.points[0]);
for point in &e.points[1..] {
path.line_to(*point);
}
}),
Stroke::default()
.with_color(if selected { style::TEAL } else { style::MUTED })
.with_width(if selected { 2.5 } else { 1.5 }),
);
} else {
"OR"
draw_arrow(
&mut f,
&e.points,
if selected { style::TEAL } else { style::MUTED },
if selected { 2.5 } else { 1.5 },
);
}
.into(),
Point::new(center.x - 10., center.y - 8.),
12.,
style::TEAL,
);
}
}
}
draw_text(
&mut f,
wrapped(&n.title, if n.control.is_some() { 25 } else { 34 }, 2),
Point::new(text_x, n.rect.y + 12.),
13.,
style::INK,
);
draw_text(
&mut f,
wrapped(&n.detail, 37, 2),
Point::new(n.rect.x + 12., n.rect.y + 60.),
11.,
style::MUTED,
);
if n.source.is_some() {
draw_text(
&mut f,
format!("Source preview · {} / {}", n.preview.len(), n.preview_total),
Point::new(n.rect.x + 12., n.rect.y + 94.),
11.,
style::TEAL,
);
let mini: BTreeMap<_, _> = n
.preview
.iter()
.enumerate()
.map(|(i, (id, _))| {
(
*id,
Rectangle::new(
Point::new(
n.rect.x + 12. + (i % 2) as f32 * 125.,
n.rect.y + 118. + (i / 2) as f32 * 44.,
PaintPass::NodeBackgrounds => {
for n in &self.nodes {
let rect =
Path::rounded_rectangle(n.rect.position(), n.rect.size(), 10.into());
f.fill(&rect, Color::from_rgb8(243, 248, 248));
f.stroke(
&rect,
Stroke::default()
.with_color(if n.id == self.selected {
style::TEAL
} else {
style::LINE
})
.with_width(if n.id == self.selected { 2.5 } else { 1. }),
);
}
}
PaintPass::NodeContents => {
for n in &self.nodes {
let text_x = n.rect.x + if n.control.is_some() { 68. } else { 12. };
if let Some(kind) = n.control {
let center = Point::new(n.rect.x + 33., n.rect.y + 34.);
let glyph = match kind {
ControlKind::Choice | ControlKind::Merge => Path::new(|p| {
p.move_to(Point::new(center.x, center.y - 22.));
p.line_to(Point::new(center.x + 23., center.y));
p.line_to(Point::new(center.x, center.y + 22.));
p.line_to(Point::new(center.x - 23., center.y));
p.close();
}),
ControlKind::Fork | ControlKind::Join => Path::rectangle(
Point::new(center.x - 23., center.y - 5.),
Size::new(46., 10.),
),
Size::new(113., 32.),
),
)
})
.collect();
for (a, b) in &n.preview_links {
let a = mini[a];
let b = mini[b];
draw_arrow(
&mut f,
&[
Point::new(a.x + a.width, a.center_y()),
Point::new(b.x, b.center_y()),
],
style::MUTED,
1.,
);
ControlKind::Loop => Path::circle(center, 23.),
};
f.stroke(
&glyph,
Stroke::default().with_color(style::TEAL).with_width(2.),
);
if matches!(kind, ControlKind::Fork | ControlKind::Join) {
f.fill(&glyph, style::TEAL);
} else {
draw_text(
&mut f,
if kind == ControlKind::Loop {
"IT"
} else {
"OR"
}
.into(),
Point::new(center.x - 10., center.y - 8.),
12.,
style::TEAL,
);
}
}
draw_text(
&mut f,
wrapped(&n.title, if n.control.is_some() { 25 } else { 34 }, 2),
Point::new(text_x, n.rect.y + 12.),
13.,
style::INK,
);
draw_text(
&mut f,
wrapped(&n.detail, 37, 2),
Point::new(n.rect.x + 12., n.rect.y + 60.),
11.,
style::MUTED,
);
if n.source.is_some() {
draw_text(
&mut f,
format!(
"{} shown · {} direct contributions",
n.preview.len(),
n.preview_total
),
Point::new(n.rect.x + 12., n.rect.y + 94.),
11.,
style::TEAL,
);
let mini: BTreeMap<_, _> = n
.preview
.iter()
.enumerate()
.map(|(i, (id, _))| {
(
id.clone(),
Rectangle::new(
Point::new(
n.rect.x + 12. + (i % 2) as f32 * 125.,
n.rect.y + 118. + (i / 2) as f32 * 44.,
),
Size::new(113., 32.),
),
)
})
.collect();
for (a, b) in &n.preview_links {
let a = mini[a];
let b = mini[b];
draw_arrow(
&mut f,
&[
Point::new(a.x + a.width, a.center_y()),
Point::new(b.x, b.center_y()),
],
style::MUTED,
1.,
);
}
for (id, name) in &n.preview {
let r = mini[id];
f.fill_rectangle(r.position(), r.size(), Color::WHITE);
f.stroke(
&Path::rectangle(r.position(), r.size()),
Stroke::default().with_color(style::LINE),
);
draw_text(
&mut f,
wrapped(name, 17, 2),
Point::new(r.x + 4., r.y + 3.),
9.,
style::INK,
);
}
if n.preview.is_empty() {
draw_text(
&mut f,
"Source has no contributions yet".into(),
Point::new(n.rect.x + 12., n.rect.y + 136.),
11.,
style::MUTED,
);
}
}
}
}
for (id, name) in &n.preview {
let r = mini[id];
f.fill_rectangle(r.position(), r.size(), Color::WHITE);
f.stroke(
&Path::rectangle(r.position(), r.size()),
Stroke::default().with_color(style::LINE),
);
draw_text(
&mut f,
wrapped(name, 17, 2),
Point::new(r.x + 4., r.y + 3.),
9.,
style::INK,
);
PaintPass::Labels => {
for e in &self.edges {
if !e.guard.is_empty() {
f.fill_rectangle(e.label.position(), e.label.size(), Color::WHITE);
draw_text(
&mut f,
wrapped(&format!("[{}]", e.guard), 19, 2),
e.label.position(),
11.,
style::INK,
);
}
}
}
if n.preview.is_empty() {
draw_text(
&mut f,
"Source has no contributions yet".into(),
Point::new(n.rect.x + 12., n.rect.y + 136.),
11.,
style::MUTED,
);
}
}
}
for e in &self.edges {
if !e.guard.is_empty() {
f.fill_rectangle(e.label.position(), e.label.size(), Color::WHITE);
draw_text(
&mut f,
wrapped(&format!("[{}]", e.guard), 19, 2),
e.label.position(),
11.,
style::INK,
);
}
}
self.overlay(state, &mut f, cursor, bounds);
@@ -1296,6 +1545,7 @@ mod tests {
guard: "request accepted".into(),
alternate_guard: "else".into(),
control: ControlKind::Choice,
contextual: None,
}
}
fn legacy_fixture() -> (Project, Id) {
@@ -1339,6 +1589,7 @@ mod tests {
owner_id,
&format!("Ordered contribution {index}"),
OccurrenceBody::Exchange {
operation: None,
exchange: Some(exchanges[index % exchanges.len()]),
sender: None,
receiver: None,
@@ -1565,7 +1816,13 @@ mod tests {
.iter()
.find(|n| n.source == Some(source_id))
.unwrap();
assert_eq!(reference.preview[0].0, inside_id);
assert_eq!(
reference.preview[0].0,
OccurrenceEndpoint {
references: vec![reference.id],
occurrence: inside_id
}
);
assert_eq!(result.children(source_id).count(), 1);
assert!(
!targets(&result, source_id, Tool::Reuse)
@@ -1687,4 +1944,102 @@ mod tests {
state.zoom(None);
assert!(state.scale(graph.size, 500.) < 1.);
}
#[test]
fn process_fit_uses_measured_height_and_reports_minimum_without_changing_manual_zoom() {
let mut state = State::default();
let graph = Size::new(420., 960.);
state.zoom(None);
let mut previous = 0.;
for height in [180., 320., 600.] {
state.viewport(Size::new(1000., height));
let scale = state.scale(graph, 200.);
assert!(scale > previous);
assert!(graph.height * scale <= height - 10. + 0.01);
assert!(!state.fit_limited(graph, 200.));
previous = scale;
}
state.viewport(Size::new(500., 180.));
let huge = Size::new(500., 10_000.);
assert_eq!(state.scale(huge, 200.), 0.05);
assert!(state.fit_limited(huge, 200.));
state.zoom(Some(1.));
for height in [180., 600.] {
state.viewport(Size::new(500., height));
assert_eq!(state.scale(huge, 200.), 1.);
assert!(!state.fit_limited(huge, 200.));
}
}
#[test]
fn process_fit_disclosures_and_focus_preserve_rejected_raw_draft_and_accepted_source() {
let (p, owner, _, _, _, _) = fixture();
let mut app = App::with_project(p);
let _ = app.update(Message::Select(owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
for input in [
Event::Start(owner, Tool::Contextual, None),
Event::Name(" pending 雨 ".into()),
Event::Contextual(contextual::Event::Guard(" unavailable && 雨 ".into())),
] {
let _ = app.update(event(input));
}
let before = app.behavior.recovery_snapshot().unwrap();
let accepted = app.document.snapshot();
assert!(app.behavior.prepare_pending(app.project()).is_err());
for (shelf, expected) in [
(Shelf::Tools, Some(Shelf::Tools)),
(Shelf::Selection, Some(Shelf::Selection)),
(Shelf::Selection, None),
] {
let focused = app.behavior.topology.focused;
let _ = app.update(event(Event::Shelf(shelf)));
assert_eq!(app.behavior.topology.shelf, expected);
let context = app.topology_context().unwrap();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context)));
assert_eq!(app.behavior.topology.focused, !focused);
assert_eq!(app.behavior.recovery_snapshot().unwrap(), before);
assert!(app.behavior.prepare_pending(app.project()).is_err());
assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &accepted));
}
}
#[test]
fn process_fit_canvas_selection_ignores_retained_ordered_page_scroll() {
let (p, owner, _, _, _, _) = fixture();
let mut state = State::default();
state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(11., 19.));
state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 370.));
let context = Context {
document: p.id,
owner,
revision: 0,
epoch: 0,
};
for scale in [0.45, 1.] {
let mut graph = Graph::new(&p, owner, owner);
graph.scale = scale;
graph.scroll_offset = state.canvas_offset();
graph.interaction = Some((context, CanvasTool::Select, true));
let node = &graph.nodes[0];
let id = node.id;
let point = node.rect.center();
let bounds = Rectangle::new(Point::new(40., 100.), Size::new(800., 300.));
let screen = Point::new(
bounds.x + point.x * scale - 11.,
bounds.y + point.y * scale - 19.,
);
let message = graph
.input(
&mut Input::default(),
&canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
bounds,
mouse::Cursor::Available(screen),
)
.and_then(|a| a.into_inner().0);
assert!(
matches!(message,Some(Message::TopologyCanvas(CanvasEvent::Select(c,actual))) if c==context && actual==id)
);
}
}
}