Files
ARCADIA-Studio/crates/oa-desktop/src/diagram/work_drag.rs
T

264 lines
9.8 KiB
Rust

use super::*;
use crate::app::allocation_drag::{self as input, Event, Mode, Press, Stamp};
use crate::diagram_usage::Target as SelectionTarget;
impl DiagramCanvas<'_> {
pub(crate) fn work_dirty_point(&self, bounds: Rectangle, screen: Point) -> bool {
let diagram = &self.project.diagrams[&self.diagram];
let view = self.projected(diagram);
let routed = routing::route_diagram(self.project, diagram, &view);
self.work_dirty_press(
&view,
&routed,
Point::new(screen.x - bounds.x, screen.y - bounds.y),
)
}
pub(super) fn work_preview(&self) -> Option<&Arc<input::Preview>> {
let capability = self.allocation?;
let feedback = capability.state.feedback.as_ref()?;
(capability.clean
&& feedback.press.stamp.context == self.context
&& feedback.press.stamp.epoch == capability.state.epoch
&& feedback.press.stamp.pan == self.pan
&& feedback.press.stamp.zoom == self.zoom)
.then_some(capability.state.preview.as_ref())
.flatten()
}
pub(super) fn allocation_surface(&self) -> bool {
self.project.diagrams[&self.diagram].projection == ProjectionKind::Allocation
}
// Dirty input must shield the complete visible handle, including its outer
// ring beyond the edge hit tolerance. This test deliberately ignores edit
// permission: the root shield constructs a non-editable canvas.
fn allocation_route_control(
&self,
view: &ProjectedView,
routed: &RoutedView,
world: Point,
) -> bool {
let Some(edge) = routed
.edges
.get(&self.selected)
.filter(|e| !e.custom_suspended)
else {
return false;
};
let radius = 8. / self.zoom;
edge.pins
.iter()
.any(|pin| world.distance(point(*pin)) <= radius)
|| [
(EdgeEnd::Source, edge.points.first()),
(EdgeEnd::Target, edge.points.last()),
]
.into_iter()
.any(|(end, at)| {
at.is_some_and(|at| world.distance(point(*at)) <= radius)
&& route_edit::attachment_node(
self.project,
&self.project.diagrams[&self.diagram],
view,
self.selected,
end,
)
.is_some()
})
}
/// Visible selected-route controls take precedence over underlying cards.
/// Returning the exact edge keeps Shift and owned movement admission aligned.
pub(super) fn allocation_press_target(
&self,
scene: &Scene,
local: Point,
) -> Option<SelectionTarget> {
let world = self.world(local);
if self.editable && self.allocation_route_control(&scene.view, &scene.routed, world) {
return Some(SelectionTarget::Edge(self.selected));
}
if let Some(port) = self.hit_port(&scene.view, local) {
Some(SelectionTarget::Port {
port: port.element,
placement: port.owner,
})
} else if let Some(id) = self.hit(&scene.view, local) {
Some(SelectionTarget::Placement(id))
} else {
self.hit_label(&scene.routed, local)
.or_else(|| self.hit_association_label(&scene.routed, local))
.or_else(|| self.hit_edge(&scene.routed, local).map(|hit| hit.0))
.map(SelectionTarget::Edge)
}
}
pub(super) fn allocation_handles_press(&self, target: &SelectionTarget) -> bool {
self.allocation_surface()
&& (self.allocation.is_some_and(|c| c.state.arrange_next)
|| matches!(target, SelectionTarget::Placement(_))
|| self.selection.is_some_and(|selection| {
selection.scope() == crate::app::multiselect::context(self.context).scope
&& selection.targets().len() > 1
&& selection.contains(target)
}))
}
pub(super) fn work_dirty_press(
&self,
view: &ProjectedView,
routed: &RoutedView,
local: Point,
) -> bool {
self.allocation.is_some_and(|c| !c.clean)
&& !self.placing
&& self.connection.is_none()
&& self.project.diagrams[&self.diagram].projection == ProjectionKind::Allocation
&& (self.allocation_route_control(view, routed, self.world(local))
|| self.hit(view, local).is_some()
|| self.hit_port(view, local).is_some()
|| self.hit_edge(routed, local).is_some()
|| self.hit_label(routed, local).is_some()
|| self.hit_association_label(routed, local).is_some())
}
pub(super) fn begin_allocation(
&self,
state: &mut State,
target: SelectionTarget,
screen: Point,
bounds: Rectangle,
) -> Action<Message> {
state.drag = None;
state.preview = None;
state.port_drag = None;
state.route_drag = None;
state.attachment_viewport = None;
state.group = None;
*state.cache.borrow_mut() = None;
let Some(capability) = self.allocation else {
return Action::capture();
};
if !capability.clean {
return Action::publish(Message::WorkDrag(Event::Refused(
capability.state.epoch,
input::FINISH_EDIT.into(),
)))
.and_capture();
}
let Some(selection) = self.selection.and_then(|selection| {
selection.snapshot(crate::app::multiselect::context(self.context))
}) else {
return Action::publish(Message::WorkDrag(Event::Refused(
capability.state.epoch,
"The selection changed. Start this drag again.".into(),
)))
.and_capture();
};
let press = Arc::new(Press {
stamp: Stamp {
context: self.context,
epoch: capability.state.epoch,
snapshot: capability.snapshot,
bounds,
pan: self.pan,
zoom: self.zoom,
},
token: Id::new_v4(),
start: screen,
selection,
target,
mode: if capability.state.arrange_next {
Mode::Arrange
} else {
Mode::Assign
},
});
state.work = Some(press.clone());
Action::publish(Message::WorkDrag(Event::Press(press))).and_capture()
}
pub(super) fn work_lifetime(
&self,
state: &mut State,
event: &canvas::Event,
bounds: Rectangle,
) -> Option<Action<Message>> {
let press = state.work.as_ref()?;
let cancel = self
.allocation
.is_none_or(|c| !c.clean || c.state.epoch != press.stamp.epoch)
|| press.stamp.context != self.context
|| press.stamp.bounds != bounds
|| press.stamp.pan != self.pan
|| press.stamp.zoom != self.zoom
|| matches!(
event,
canvas::Event::Window(iced::window::Event::Unfocused)
| canvas::Event::Mouse(mouse::Event::CursorLeft)
| canvas::Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Middle | mouse::Button::Right
))
| canvas::Event::Mouse(mouse::Event::WheelScrolled { .. })
)
|| matches!(event, canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { key, .. })
if *key == iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape));
if cancel {
let token = press.token;
state.work = None;
state.pointer = None;
return Some(Action::publish(Message::WorkDrag(Event::Cancel(token))).and_capture());
}
None
}
}
/// Passive frozen target outlines. Destination identity is in the toolbar so
/// feedback cannot cover candidate text or change the target hit geometry.
pub struct WorkFeedback<'a>(pub DiagramCanvas<'a>);
impl canvas::Program<Message> for WorkFeedback<'_> {
type State = ();
fn draw(
&self,
_: &(),
renderer: &Renderer,
_: &Theme,
bounds: Rectangle,
_: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let Some(capability) = self.0.allocation else {
return vec![];
};
let Some(feedback) = &capability.state.feedback else {
return vec![];
};
if feedback.press.stamp.epoch != capability.state.epoch
|| feedback.press.stamp.context != self.0.context
|| feedback.press.stamp.bounds != bounds
{
return vec![];
}
let mut frame = Frame::new(renderer, bounds.size());
let color = if feedback.refused {
Color::from_rgb8(173, 66, 41)
} else {
style::TEAL
};
let Some(frozen) = &feedback.frozen else {
return vec![];
};
for index in &feedback.targets {
let target = &frozen.targets[*index];
let region = target.region;
let top = Point::new(
region.x * self.0.zoom + self.0.pan.x,
region.y * self.0.zoom + self.0.pan.y,
);
frame.stroke(
&Path::rectangle(
top,
iced::Size::new(region.width * self.0.zoom, region.height * self.0.zoom),
),
Stroke::default().with_color(color).with_width(3.),
);
}
vec![frame.into_geometry()]
}
}