Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/topology/direct.rs
T
sorokin.oleg.edu 1711bff650 Add exact Process selection and atomic column-slot group reorder
Preserve the full-desktop-1 captured source. Qualified previews inspect their exact subject read-only; group movement preserves source order and other columns. Native acceptance and strict-lint payload-size corrections follow separately.
2026-09-23 05:39:10 -07:00

1077 lines
38 KiB
Rust

//! Local pointer state and ordinary command proposals for the derived process graph.
use super::*;
#[path = "direct_selection.rs"]
mod selection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Context {
pub document: Id,
pub owner: Id,
pub revision: u64,
pub epoch: u64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Insertion {
pub link: Option<Id>,
pub before: Option<Id>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CanvasTool {
#[default]
Select,
Add(Tool, ControlKind),
Connect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum End {
Source,
Target,
}
#[derive(Debug, Clone)]
pub enum CanvasEvent {
Scrolled(Context, scenario::ScrollArea, iced::Vector),
Choose(Context, CanvasTool),
Select(Context, Id),
Remove(Context, Id),
SelectQualified(Context, OccurrenceEndpoint),
ExpandQualified(Context, OccurrenceEndpoint),
OpenQualified(Context, OccurrenceEndpoint),
AppendExchange(Context),
Begin {
context: Context,
tool: Tool,
control: ControlKind,
insertion: Insertion,
},
Connect {
context: Context,
source: Id,
target: Id,
},
Reconnect {
context: Context,
link: Id,
end: End,
target: Id,
},
Reorder {
context: Context,
occurrence: Id,
before: Id,
},
Expand(Context, Id),
Open(Context, Id),
Back(Context),
Zoom(Context, Option<f32>),
Camera(Context, u64, super::super::canvas_workspace::Event),
Viewport(Context, Size),
}
impl CanvasEvent {
pub fn context(&self) -> Context {
match self {
Self::Scrolled(c, ..)
| Self::Choose(c, ..)
| Self::Select(c, ..)
| Self::Remove(c, ..)
| Self::SelectQualified(c, ..)
| Self::ExpandQualified(c, ..)
| Self::OpenQualified(c, ..)
| Self::Expand(c, ..)
| Self::Open(c, ..)
| Self::Back(c)
| Self::AppendExchange(c)
| Self::Zoom(c, ..)
| Self::Camera(c, ..)
| Self::Viewport(c, ..) => *c,
Self::Begin { context, .. }
| Self::Connect { context, .. }
| Self::Reconnect { context, .. }
| Self::Reorder { context, .. } => *context,
}
}
}
pub(super) struct ReturnFrame {
identity: Option<(Id, Id)>,
owner: Id,
occurrence: Id,
target: Id,
address: Option<oa_core::topology_context::ContextAddress>,
pan: iced::Vector,
zoom: Option<f32>,
fit: bool,
viewport: Option<Size>,
expanded: BTreeSet<Id>,
contextual_expanded: BTreeSet<OccurrenceEndpoint>,
}
impl State {
pub(crate) fn select_context(&mut self, address: oa_core::topology_context::ContextAddress) {
self.selected_context = Some(address);
}
pub(crate) fn clear_context(&mut self) {
self.selected_context = None;
}
pub(super) fn qualified_selection(
&self,
p: &Project,
owner: Id,
) -> Option<&oa_core::topology_context::ContextAddress> {
self.selected_context.as_ref().filter(|address| {
address.owner == owner
&& oa_core::topology_context::TopologyIndex::new(p)
.resolve(owner, &address.endpoint)
.is_ok()
})
}
pub(crate) fn expand_context(
&mut self,
p: &Project,
owner: Id,
endpoint: OccurrenceEndpoint,
) -> bool {
if oa_core::topology_context::TopologyIndex::new(p)
.resolve(owner, &endpoint)
.is_err()
|| !matches!(
p.elements
.get(&endpoint.occurrence)
.and_then(|e| e.shared()),
Some(SharedBody::Occurrence {
body: OccurrenceBody::SequenceReference { sequence: Some(_) },
..
})
)
{
return false;
}
if let Some(root) = endpoint.references.first() {
self.expanded.insert(*root);
}
if !self.contextual_expanded.remove(&endpoint) {
self.contextual_expanded.insert(endpoint);
}
true
}
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
if self.identity != Some((document, owner)) {
self.identity = Some((document, owner));
self.tool = CanvasTool::Select;
self.pan = iced::Vector::ZERO;
self.zoom = Some(1.);
self.fit = false;
self.viewport = None;
self.expanded.clear();
self.details_offset = iced::Vector::ZERO;
}
}
#[cfg(test)]
pub(crate) fn details_offset(&self) -> iced::Vector {
self.details_offset
}
pub(crate) fn scrolled(
&mut self,
scenario::ScrollArea::Page: scenario::ScrollArea,
offset: iced::Vector,
) -> bool {
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
return false;
}
let changed = self.details_offset != offset;
self.details_offset = offset;
changed
}
pub(crate) fn choose(&mut self, tool: CanvasTool) {
// Choosing how to edit must retain the user's current view.
self.tool = tool;
}
pub(crate) fn camera(&mut self, event: super::super::canvas_workspace::Event) -> bool {
super::super::canvas_workspace::change(&mut self.pan, &mut self.zoom, &mut self.fit, event)
}
pub(crate) fn zoom(&mut self, value: Option<f32>) {
self.fit = value.is_none();
self.zoom = value.map(|v| v.clamp(0.05, 3.));
}
pub(crate) fn viewport(&mut self, size: Size) -> bool {
if !size.width.is_finite()
|| !size.height.is_finite()
|| size.width <= 0.
|| size.height <= 0.
{
return false;
}
let changed = self.viewport != Some(size);
self.viewport = Some(size);
changed
}
pub(crate) fn expand(&mut self, p: &Project, owner: Id, id: Id) -> bool {
if !is_contribution(p, owner, id)
|| !matches!(
p.elements[&id].shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::SequenceReference { .. },
..
})
)
{
return false;
}
if !self.expanded.remove(&id) {
self.expanded.insert(id);
}
true
}
pub(crate) fn remember(&mut self, owner: Id, occurrence: Id, target: Id) {
if self.return_to.len() == 32 {
self.return_to.remove(0);
}
self.return_to.push(ReturnFrame {
identity: self.identity,
owner,
occurrence,
target,
address: self.selected_context.clone(),
pan: self.pan,
zoom: self.zoom,
fit: self.fit,
viewport: self.viewport,
expanded: self.expanded.clone(),
contextual_expanded: self.contextual_expanded.clone(),
});
}
pub(crate) fn back(&mut self, p: &Project) -> Option<(Id, Id)> {
while let Some(frame) = self.return_to.pop() {
if frame.identity.map(|i| i.0) != self.identity.map(|i| i.0) {
continue;
}
let valid = referenced(p, frame.owner, frame.occurrence) == Some(frame.target)
|| if let Some(address) = &frame.address {
oa_core::topology_context::TopologyIndex::new(p)
.resolve(frame.owner, &address.endpoint)
.is_ok_and(|r| r.defining_sequence == frame.target)
} else {
false
};
if valid && is_contribution(p, frame.owner, frame.occurrence) {
self.identity = frame.identity;
self.selected_context = frame.address.filter(|a| {
oa_core::topology_context::TopologyIndex::new(p)
.resolve(frame.owner, &a.endpoint)
.is_ok()
});
self.pan = frame.pan;
self.zoom = frame.zoom;
self.fit = frame.fit;
self.viewport = frame.viewport;
self.expanded = frame.expanded;
self.contextual_expanded = frame.contextual_expanded;
return Some((frame.owner, frame.occurrence));
}
}
None
}
pub(crate) fn camera_zoom(&mut self, p: &Project, owner: Id, zoom: Option<f32>) {
let graph =
Graph::expanded_qualified(p, owner, owner, &self.expanded, &self.contextual_expanded);
let old = self.scale(graph.size, 500.);
if let Some(next) = zoom {
let size = self.viewport.unwrap_or(Size::new(500., 300.));
self.camera(super::super::canvas_workspace::Event::Zoom {
factor: next / old,
at: Point::new(size.width / 2., size.height / 2.),
base: old,
});
} else {
self.zoom(None);
let fitted = self.scale(graph.size, 500.);
self.zoom(Some(fitted));
self.pan = iced::Vector::ZERO;
}
}
pub(crate) fn created_selection(&self, p: &Project, command: &Command) -> Option<Id> {
let owner = self.pending_owner()?;
if self
.draft
.as_ref()
.is_some_and(|d| d.source.label(d.target).is_some())
&& self.prepare_pending(p).ok().as_ref() != Some(command)
{
return None;
}
if let Command::CreateRelation(r) | Command::EditRelation(_, r) = command
&& matches!(&r.body, RelationBody::ContextualLink { owner: caller, .. } if *caller == owner)
{
return Some(r.id);
}
let Command::Batch(commands) = command else {
return None;
};
commands.iter().find_map(|c| match c {
Command::CreateElement(e) if e.parent == Some(owner) => Some(e.id),
_ => None,
})
}
pub(crate) fn begin(
&mut self,
p: &Project,
context: Context,
tool: Tool,
control: ControlKind,
insertion: Insertion,
) -> Result<(), String> {
if self.draft.is_some() {
return Err(PENDING_DRAFT.into());
}
if !matches!(
tool,
Tool::Work | Tool::Exchange | Tool::Reuse | Tool::Control
) {
return Err("Choose a contribution tool.".into());
}
let order = legacy_order(p, context.owner);
let link = insertion
.link
.map(|id| {
p.relations
.get(&id)
.filter(|r| {
r.kind() == RelationKind::SequenceLink
&& is_contribution(p, context.owner, r.source())
&& is_contribution(p, context.owner, r.target())
})
.ok_or("Choose a sequence link in this process.")
})
.transpose()?;
if insertion
.before
.is_some_and(|id| !is_contribution(p, context.owner, id))
{
return Err("Choose a presentation slot in this process.".into());
}
self.update(p, Event::Start(context.owner, tool, None))?;
let d = self.draft.as_mut().expect("started form");
d.fixed = true;
d.insertion = insertion;
d.control = control;
if let Some(r) = link {
d.before = Some(r.source());
d.after = Some(r.target());
} else if !order.is_empty() {
let index = insertion
.before
.and_then(|id| order.iter().position(|v| *v == id))
.unwrap_or(order.len());
d.before = index.checked_sub(1).map(|i| order[i]);
d.after = order.get(index).copied();
}
Ok(())
}
pub(crate) fn append_exchange(&mut self, p: &Project, owner: Id) -> Result<(), String> {
let last = legacy_order(p, owner)
.last()
.copied()
.ok_or("Choose an ordered exchange path to append to.")?;
self.update(p, Event::Start(owner, Tool::Exchange, Some(last)))?;
self.draft.as_mut().expect("started form").fixed = true;
Ok(())
}
pub(crate) fn connect(
&mut self,
p: &Project,
owner: Id,
source: Id,
target: Id,
) -> Result<(), String> {
if self.draft.is_some() {
return Err(PENDING_DRAFT.into());
}
if !is_contribution(p, owner, source)
|| !is_contribution(p, owner, target)
|| !validation::can_connect(p, RelationKind::SequenceLink, source, target)
{
return Err("Choose compatible contributions directly owned by this process.".into());
}
self.update(p, Event::Start(owner, Tool::Connect, None))?;
let d = self.draft.as_mut().expect("started form");
d.fixed = true;
d.before = Some(source);
d.after = Some(target);
Ok(())
}
}
pub(crate) fn referenced(p: &Project, owner: Id, occurrence: Id) -> Option<Id> {
if !is_contribution(p, owner, occurrence) {
return None;
}
match p.elements[&occurrence].shared()? {
SharedBody::Occurrence {
body: OccurrenceBody::Action { action, .. },
..
} => *action,
SharedBody::Occurrence {
body: OccurrenceBody::Exchange { exchange, .. },
..
} => *exchange,
SharedBody::Occurrence {
body: OccurrenceBody::SequenceReference { sequence },
..
} => *sequence,
_ => None,
}
}
pub(crate) fn reconnect(
p: &Project,
owner: Id,
id: Id,
end: End,
endpoint: Id,
) -> Result<Command, String> {
let mut r = p
.relations
.get(&id)
.filter(|r| {
r.kind() == RelationKind::SequenceLink
&& is_contribution(p, owner, r.source())
&& is_contribution(p, owner, r.target())
})
.ok_or("Choose a sequence link in this process.")?
.clone();
if !is_contribution(p, owner, endpoint) {
return Err("Choose a local contribution endpoint.".into());
}
if let RelationBody::SequenceLink { source, target, .. } = &mut r.body {
match end {
End::Source => *source = endpoint,
End::Target => *target = endpoint,
}
}
let command = Command::EditRelation(id, r);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
pub(crate) fn reorder(
p: &Project,
owner: Id,
id: Id,
before: Id,
) -> Result<super::group_reorder::Plan, String> {
super::group_reorder::Prepared::new(
p,
owner,
&[crate::diagram_usage::Target::ProcessOccurrence(
OccurrenceEndpoint {
references: vec![],
occurrence: id,
},
)],
)?
.plan(super::group_reorder::Destination::Before(before))
}
#[derive(Default)]
pub(super) struct Input {
context: Option<Context>,
tool: CanvasTool,
pointer: Option<Point>,
bounds: Option<Rectangle>,
source: Option<Id>,
press: Option<Press>,
modifiers: iced::keyboard::Modifiers,
group: Option<selection::GroupPress>,
}
#[derive(Clone, Copy)]
enum Press {
Connect {
source: Id,
first: bool,
start: Point,
moved: bool,
},
Reconnect {
link: Id,
end: End,
start: Point,
moved: bool,
},
Reorder {
occurrence: Id,
start: Point,
moved: bool,
},
Add {
insertion: Insertion,
},
}
impl Input {
fn cancel(&mut self) {
self.source = None;
self.press = None;
self.group = None;
}
}
impl Graph<'_> {
fn removal_target(&self, owner: Id, id: Id) -> bool {
id != owner
&& super::super::selected_behavior(self.project, id) == Some(owner)
&& (self.nodes.iter().any(|node| node.id == id)
|| self.edges.iter().any(|edge| edge.id == id))
}
fn ordered_path_at(&self, point: Point) -> Option<&OrderedPath> {
self.ordered_paths.iter().find(|path| {
path.points.windows(2).any(|pair| {
crate::diagram::distance_to_segment(point, pair[0], pair[1]) < 8. / self.scale
})
})
}
fn node_at(&self, point: Point) -> Option<Id> {
self.nodes
.iter()
.rev()
.find(|n| n.rect.contains(point))
.map(|n| n.id)
}
fn qualified_at(&self, point: Point) -> Option<OccurrenceEndpoint> {
self.nodes.iter().find_map(|node| {
node.preview.iter().enumerate().find_map(|(i, (id, _))| {
let rect = Rectangle::new(
Point::new(
node.rect.x + 12. + (i % 2) as f32 * 125.,
node.rect.y + 118. + (i / 2) as f32 * 44.,
),
Size::new(113., 32.),
);
rect.contains(point).then(|| id.clone())
})
})
}
fn edge_at(&self, point: Point) -> Option<Id> {
self.edges
.iter()
.find(|e| {
(!e.guard.is_empty() && e.label.contains(point))
|| e.points.windows(2).any(|v| {
crate::diagram::distance_to_segment(point, v[0], v[1]) < 8. / self.scale
})
})
.map(|e| e.id)
}
fn contextual_edge_at(&self, point: Point) -> Option<Id> {
self.edges
.iter()
.filter(|edge| edge.layer == EdgeLayer::Contextual)
.find(|edge| {
edge.label.contains(point)
|| edge.points.windows(2).any(|segment| {
crate::diagram::distance_to_segment(point, segment[0], segment[1])
< 8. / self.scale
})
})
.map(|edge| edge.id)
}
fn handle(&self, id: Id) -> Option<Rectangle> {
let n = self.nodes.iter().find(|n| n.id == id)?;
Some(Rectangle::new(
Point::new(n.rect.x + n.rect.width - 30., n.rect.y + 7.),
Size::new(23., 24.),
))
}
fn endpoint_at(&self, point: Point) -> Option<(Id, End)> {
if self.project.relations.get(&self.selected)?.kind() != RelationKind::SequenceLink {
return None;
}
let e = self.edges.iter().find(|e| e.id == self.selected)?;
[(e.points[0], End::Source), (*e.points.last()?, End::Target)]
.into_iter()
.find(|(p, _)| p.distance(point) < 10. / self.scale)
.map(|(_, end)| (e.id, end))
}
fn connectable(&self, source: Id, target: Id) -> bool {
validation::can_connect(self.project, RelationKind::SequenceLink, source, target)
}
pub(super) fn input(
&self,
state: &mut Input,
event: &canvas::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
if let Some(action) = self.selection_input(state, event, bounds, cursor) {
return Some(action);
}
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.bounds.is_some_and(|b| b != bounds)
{
state.cancel();
state.pointer = None;
state.context = context;
state.tool = tool;
}
state.bounds = Some(bounds);
if !editable {
state.cancel();
}
let visible = cursor.position().is_some();
if !visible
&& matches!(
event,
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
)
{
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::Connect { start, moved, .. }
| Press::Reconnect { start, moved, .. }
| Press::Reorder { start, moved, .. },
) = &mut state.press
{
*moved |= position.distance(*start) >= 3.;
}
mouse::Cursor::Available(*position)
}
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. } | mouse::Event::CursorLeft)
| canvas::Event::Window(
iced::window::Event::Resized(_) | iced::window::Event::Unfocused,
) => {
state.cancel();
state.pointer = None;
return Some(Action::request_redraw());
}
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Delete),
modifiers,
repeat: false,
..
}) if editable
&& tool == CanvasTool::Select
&& (modifiers.is_empty() || *modifiers == iced::keyboard::Modifiers::SHIFT)
&& state.press.is_none()
&& state.source.is_none() =>
{
let context = context?;
if self.removal_target(context.owner, self.selected) {
return Some(
Action::publish(Message::TopologyCanvas(CanvasEvent::Remove(
context,
self.selected,
)))
.and_capture(),
);
}
return None;
}
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
..
}) => {
state.cancel();
return Some(Action::request_redraw().and_capture());
}
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
}
_ => cursor,
};
let world = cursor.position_in(bounds).map(|p| {
Point::new(
(p.x - self.camera_pan.x) / self.scale,
(p.y - self.camera_pan.y) / self.scale,
)
});
let publish = |event| Some(Action::publish(Message::TopologyCanvas(event)).and_capture());
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if visible => {
let point = world?;
let Some(context) = context else {
return self
.node_at(point)
.or_else(|| self.edge_at(point))
.map(|id| Action::publish(Message::Select(id)).and_capture());
};
if !editable {
return None;
}
let start = cursor.position()?;
match tool {
CanvasTool::Add(..) => {
state.press = Some(Press::Add {
insertion: Insertion {
link: self
.edge_at(point)
.filter(|_| self.node_at(point).is_none()),
before: self.node_at(point).or_else(|| {
self.ordered_path_at(point).map(|path| path.target)
}),
},
});
}
CanvasTool::Connect => {
let first = state.source.is_none();
let source = state.source.or_else(|| self.node_at(point))?;
state.source = Some(source);
state.press = Some(Press::Connect {
source,
first,
start,
moved: false,
});
}
CanvasTool::Select => {
if let Some(endpoint) = self.qualified_at(point) {
return publish(CanvasEvent::SelectQualified(context, endpoint));
} else if let Some((link, end)) = self.endpoint_at(point) {
state.press = Some(Press::Reconnect {
link,
end,
start,
moved: false,
});
} else if let Some(link) = self.contextual_edge_at(point) {
return publish(CanvasEvent::Select(context, link));
} else if self
.handle(self.selected)
.is_some_and(|r| r.contains(point))
{
state.press = Some(Press::Reorder {
occurrence: self.selected,
start,
moved: false,
});
} else {
return self
.node_at(point)
.or_else(|| self.edge_at(point))
.and_then(|id| publish(CanvasEvent::Select(context, id)));
}
}
}
Some(Action::request_redraw().and_capture())
}
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let press = state.press.take()?;
let context = context?;
match press {
Press::Add { insertion } if world.is_some() => {
if let CanvasTool::Add(tool, control) = tool {
state.cancel();
return publish(CanvasEvent::Begin {
context,
tool,
control,
insertion,
});
}
}
Press::Connect {
source,
first,
moved,
..
} if moved || !first => {
if let Some(target) = world
.and_then(|p| self.node_at(p))
.filter(|target| self.connectable(source, *target))
{
state.cancel();
return publish(CanvasEvent::Connect {
context,
source,
target,
});
}
}
Press::Reconnect {
link,
end,
moved: true,
..
} => {
if let Some(target) = world.and_then(|p| self.node_at(p)) {
state.cancel();
return publish(CanvasEvent::Reconnect {
context,
link,
end,
target,
});
}
}
Press::Reorder {
occurrence,
moved: true,
..
} => {
if let Some(before) = world.and_then(|p| self.node_at(p)) {
state.cancel();
return publish(CanvasEvent::Reorder {
context,
occurrence,
before,
});
}
}
_ => {}
}
Some(Action::request_redraw().and_capture())
}
canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) => {
Some(Action::request_redraw())
}
_ => None,
}
}
pub(super) fn overlay(
&self,
state: &Input,
f: &mut Frame,
cursor: mouse::Cursor,
bounds: Rectangle,
) {
self.selection_overlay(state, f, bounds);
let Some((context, tool, editable)) = self.interaction else {
return;
};
if let Some(insertion) = self.pending_insertion {
if let Some(edge) = insertion
.link
.and_then(|id| self.edges.iter().find(|e| e.id == id))
{
draw_arrow(f, &edge.points, style::TEAL, 3.);
} else if let Some(node) = insertion
.before
.and_then(|id| self.nodes.iter().find(|n| n.id == id))
{
f.stroke(
&Path::rounded_rectangle(node.rect.position(), node.rect.size(), 10.into()),
Stroke::default().with_color(style::TEAL).with_width(3.),
);
}
}
if !editable {
return;
}
let point = state
.pointer
.or_else(|| cursor.position())
.filter(|p| bounds.contains(*p))
.map(|p| {
Point::new(
(p.x - bounds.x - self.camera_pan.x) / self.scale,
(p.y - bounds.y - self.camera_pan.y) / self.scale,
)
});
let live =
state.context == Some(context) && state.tool == tool && state.bounds == Some(bounds);
if live && let Some(source) = state.source {
for n in &self.nodes {
if n.id == source || self.connectable(source, n.id) {
f.stroke(
&Path::rounded_rectangle(n.rect.position(), n.rect.size(), 10.into()),
Stroke::default().with_color(style::TEAL).with_width(2.5),
);
}
}
if let (Some(n), Some(to)) = (self.nodes.iter().find(|n| n.id == source), point) {
draw_arrow(
f,
&[Point::new(n.rect.x + n.rect.width, n.rect.center_y()), to],
style::TEAL,
2.,
);
}
}
if live
&& let Some(Press::Reconnect { link, end, .. }) = state.press
&& let Some(edge) = self.edges.iter().find(|e| e.id == link)
{
for node in &self.nodes {
let (source, target) = match end {
End::Source => (node.id, edge.target),
End::Target => (edge.source, node.id),
};
if self.connectable(source, target) {
f.stroke(
&Path::rounded_rectangle(node.rect.position(), node.rect.size(), 10.into()),
Stroke::default().with_color(style::TEAL).with_width(2.5),
);
}
}
if let Some(point) = point {
let (from, to) = match end {
End::Source => (point, *edge.points.last().unwrap()),
End::Target => (edge.points[0], point),
};
draw_arrow(f, &[from, to], style::TEAL, 2.5);
}
}
if live
&& let Some(Press::Reorder { occurrence, .. }) = state.press
&& let Some(point) = point
&& let Some(target) = self.node_at(point)
{
let source = self.nodes.iter().find(|n| n.id == occurrence).unwrap();
let target = self.nodes.iter().find(|n| n.id == target).unwrap();
if source.rect.x == target.rect.x {
f.stroke(
&Path::line(
Point::new(target.rect.x, target.rect.y - 7.),
Point::new(target.rect.x + target.rect.width, target.rect.y - 7.),
),
Stroke::default().with_color(style::TEAL).with_width(3.),
);
draw_text(
f,
"Order before · paths unchanged".into(),
Point::new(target.rect.x, target.rect.y - 24.),
12.,
style::TEAL,
);
}
}
if tool == CanvasTool::Select
&& self
.selection
.as_ref()
.is_none_or(|s| s.targets().len() == 1)
{
if let Some(r) = self.handle(self.selected) {
f.fill_rectangle(r.position(), r.size(), Color::WHITE);
draw_text(f, "↕".into(), r.position(), 17., style::TEAL);
}
if let Some(e) = self.edges.iter().find(|e| e.id == self.selected) {
for p in [e.points[0], *e.points.last().unwrap()] {
f.fill(&Path::circle(p, 6. / self.scale), style::TEAL);
}
}
}
if let CanvasTool::Add(..) = tool
&& let Some(point) = point
{
let at = self
.edge_at(point)
.filter(|_| self.node_at(point).is_none());
let caption = if let Some(id) = at {
if let Some(e) = self.edges.iter().find(|e| e.id == id) {
draw_arrow(f, &e.points, style::TEAL, 3.);
}
"Insert on this path · preserve its guard"
} else if let Some(path) = self.ordered_path_at(point) {
draw_arrow(f, &path.points, style::TEAL, 3.);
"Insert on ordered path · preserve its order"
} else if self.ordered && self.node_at(point).is_some() {
"Insert before this contribution · preserve path"
} else if self.ordered {
"Continue the ordered path"
} else if self.node_at(point).is_some() {
"Add before this contribution · unconnected"
} else {
"Append contribution · unconnected"
};
draw_text(
f,
caption.into(),
Point::new(point.x + 12., point.y + 16.),
12. / self.scale,
style::TEAL,
);
}
}
}
impl State {
pub(crate) fn selecting(&self) -> bool {
self.tool == CanvasTool::Select
}
pub(crate) fn removal_target(&self, p: &Project, owner: Id, id: Id) -> bool {
self.qualified_selection(p, owner).is_none()
&& Graph::expanded_qualified(p, owner, id, &self.expanded, &self.contextual_expanded)
.removal_target(owner, id)
}
pub(crate) fn has_qualified_selection(&self, p: &Project, owner: Id) -> bool {
self.qualified_selection(p, owner).is_some()
}
#[cfg(test)]
pub(crate) fn removal_test_point(
&self,
app: &App,
owner: Id,
id: Id,
qualified: Option<&OccurrenceEndpoint>,
) -> Point {
let graph = Graph::expanded_qualified(
app.project(),
owner,
id,
&self.expanded,
&self.contextual_expanded,
);
let point = graph
.nodes
.iter()
.find(|node| node.id == id)
.map(|node| {
qualified.map_or(
Point::new(node.rect.x + 20., node.rect.y + 25.),
|endpoint| node.endpoint_rect(endpoint).center(),
)
})
.or_else(|| {
graph
.edges
.iter()
.find(|edge| edge.id == id)
.map(|edge| edge.label.center())
})
.expect("rendered process target");
Point::ORIGIN
+ self.pan
+ iced::Vector::new(point.x, point.y)
* self.scale(graph.size, (app.window_size.width - 600.).max(200.))
}
pub(crate) fn reveal_selection(&mut self, p: &Project, owner: Id, id: Id) -> bool {
let graph =
Graph::expanded_qualified(p, owner, id, &self.expanded, &self.contextual_expanded);
let scale = self.scale(graph.size, 500.);
let rect = graph
.nodes
.iter()
.find(|n| n.id == id)
.map(|n| {
self.qualified_selection(p, owner)
.map_or(n.rect, |a| n.endpoint_rect(&a.endpoint))
})
.or_else(|| graph.edges.iter().find(|e| e.id == id).map(|e| e.label));
let Some(rect) = rect else {
return false;
};
let viewport = self.viewport.unwrap_or(Size::new(500., 300.));
let x = rect.x * scale + self.pan.x;
let y = rect.y * scale + self.pan.y;
let delta = iced::Vector::new(
if x < 12. || x + rect.width * scale > viewport.width - 12. {
12. - x
} else {
0.
},
if y < 12. || y + rect.height * scale > viewport.height - 12. {
12. - y
} else {
0.
},
);
self.camera(super::super::canvas_workspace::Event::Pan(delta))
}
}