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
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:
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user