880 lines
31 KiB
Rust
880 lines
31 KiB
Rust
//! Local pointer state and ordinary command proposals for the derived process graph.
|
|
use super::*;
|
|
|
|
#[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),
|
|
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>),
|
|
Focus(Context),
|
|
Viewport(Context, Size),
|
|
}
|
|
impl CanvasEvent {
|
|
pub fn context(&self) -> Context {
|
|
match self {
|
|
Self::Scrolled(c, ..)
|
|
| Self::Choose(c, ..)
|
|
| Self::Select(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::Focus(c)
|
|
| Self::Viewport(c, ..) => *c,
|
|
Self::Begin { context, .. }
|
|
| Self::Connect { context, .. }
|
|
| Self::Reconnect { context, .. }
|
|
| Self::Reorder { context, .. } => *context,
|
|
}
|
|
}
|
|
}
|
|
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.zoom = Some(1.);
|
|
self.fit = false;
|
|
self.viewport = None;
|
|
self.expanded.clear();
|
|
self.scroll_offsets = [iced::Vector::ZERO; 2];
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
pub(crate) fn scroll_offset(&self) -> iced::Vector {
|
|
self.scroll_offsets[0] + self.scroll_offsets[1]
|
|
}
|
|
pub(crate) fn scrolled(&mut self, area: scenario::ScrollArea, offset: iced::Vector) -> bool {
|
|
let index = match area {
|
|
scenario::ScrollArea::Canvas => 0,
|
|
scenario::ScrollArea::Page => 1,
|
|
};
|
|
let changed = self.scroll_offsets[index] != offset;
|
|
self.scroll_offsets[index] = 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 zoom(&mut self, value: Option<f32>) {
|
|
self.fit = value.is_none();
|
|
self.zoom = value.map(|v| v.clamp(0.5, 2.));
|
|
}
|
|
pub(crate) fn viewport(&mut self, size: Size) -> bool {
|
|
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) {
|
|
self.return_to
|
|
.push((owner, occurrence, self.selected_context.clone()));
|
|
}
|
|
pub(crate) fn back(&mut self, p: &Project) -> Option<(Id, Id)> {
|
|
while let Some((owner, occurrence, address)) = self.return_to.pop() {
|
|
if is_contribution(p, owner, occurrence) {
|
|
self.selected_context = address.filter(|address| {
|
|
address.owner == owner
|
|
&& oa_core::topology_context::TopologyIndex::new(p)
|
|
.resolve(owner, &address.endpoint)
|
|
.is_ok()
|
|
});
|
|
return Some((owner, occurrence));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
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<Command, String> {
|
|
if !is_contribution(p, owner, id) || !is_contribution(p, owner, before) {
|
|
return Err("Reorder contributions in this process only.".into());
|
|
}
|
|
let graph = Graph::new(p, owner, id);
|
|
let position = |id| graph.nodes.iter().find(|n| n.id == id).map(|n| n.rect.x);
|
|
if position(id) != position(before) {
|
|
return Err("Reorder within this column; links determine the path columns.".into());
|
|
}
|
|
let mut steps = p.elements[&owner].steps().unwrap().clone();
|
|
if id != before {
|
|
steps.retain(|v| *v != id);
|
|
let index = steps
|
|
.iter()
|
|
.position(|v| *v == before)
|
|
.ok_or("This slot no longer exists.")?;
|
|
steps.insert(index, id);
|
|
}
|
|
let command = Command::SetSteps(owner, steps);
|
|
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
|
|
Ok(command)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct Input {
|
|
context: Option<Context>,
|
|
tool: CanvasTool,
|
|
pointer: Option<Point>,
|
|
bounds: Option<Rectangle>,
|
|
source: Option<Id>,
|
|
press: Option<Press>,
|
|
}
|
|
#[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;
|
|
}
|
|
}
|
|
impl Graph<'_> {
|
|
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>> {
|
|
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 + self.scroll_offset)
|
|
}
|
|
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::Escape),
|
|
..
|
|
}) => {
|
|
state.cancel();
|
|
return Some(Action::request_redraw().and_capture());
|
|
}
|
|
canvas::Event::Mouse(
|
|
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
|
|
) => state
|
|
.pointer
|
|
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
|
|
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
|
|
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
|
|
cursor
|
|
}
|
|
_ => cursor,
|
|
};
|
|
let world = cursor
|
|
.position_in(bounds)
|
|
.map(|p| Point::new(p.x / self.scale, p.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()? - self.scroll_offset;
|
|
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,
|
|
) {
|
|
let Some((context, tool, editable)) = self.interaction else {
|
|
return;
|
|
};
|
|
if !editable {
|
|
return;
|
|
}
|
|
let point = state
|
|
.pointer
|
|
.map(|p| p + self.scroll_offset)
|
|
.or_else(|| cursor.position())
|
|
.filter(|p| bounds.contains(*p))
|
|
.map(|p| Point::new((p.x - bounds.x) / self.scale, (p.y - bounds.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 {
|
|
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 selection_offset(
|
|
&self,
|
|
p: &Project,
|
|
owner: Id,
|
|
id: Id,
|
|
) -> Option<scrollable::AbsoluteOffset> {
|
|
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))?;
|
|
Some(scrollable::AbsoluteOffset {
|
|
x: (rect.x * scale - 12.).max(0.),
|
|
y: (rect.y * scale - 12.).max(0.),
|
|
})
|
|
}
|
|
}
|