Add captured process and scenario canvas removal

This commit is contained in:
2026-09-12 05:17:11 -07:00
parent 28e21a0ae8
commit 49a4cb17b1
10 changed files with 770 additions and 18 deletions
@@ -10,6 +10,7 @@ use iced::{keyboard, mouse};
struct Input {
scope: Option<Scope>,
focused: bool,
focus_pending: bool,
pointer: Option<Point>,
pan: Option<(mouse::Button, Point)>,
space: bool,
@@ -21,9 +22,11 @@ impl operation::Focusable for Input {
}
fn focus(&mut self) {
self.focused = true;
self.focus_pending = false;
}
fn unfocus(&mut self) {
self.focused = false;
self.focus_pending = false;
self.pan = None;
self.space = false;
}
@@ -92,6 +95,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
}
if !self.keyboard {
state.focused = false;
state.focus_pending = false;
}
state.scope = Some(self.scope);
tree.diff_children(std::slice::from_ref(&self.content));
@@ -143,6 +147,11 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
return;
}
let state = tree.state.downcast_mut::<Input>();
// A traversal operation is applied after this Iced event batch. Keep
// its focus witness, but retire keyboard admission immediately.
if state.focus_pending && matches!(event, Event::Keyboard(_)) {
return;
}
let changed_bounds = state.bounds.is_some_and(|bounds| bounds != layout.bounds());
state.bounds = Some(layout.bounds());
if changed_bounds || !self.keyboard {
@@ -151,6 +160,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
}
if !self.keyboard {
state.focused = false;
state.focus_pending = false;
}
if let Event::Mouse(mouse::Event::CursorMoved { position }) = event {
state.pointer = Some(*position);
@@ -178,6 +188,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
state.pan = None;
state.space = false;
state.focused = false;
state.focus_pending = false;
}
Event::Mouse(mouse::Event::CursorLeft) => {
state.pan = None;
@@ -190,6 +201,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
cancel = true;
}
state.focused = inside && self.keyboard;
state.focus_pending = false;
if inside
&& self.keyboard
&& (*button == mouse::Button::Middle
@@ -242,6 +254,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
repeat: false,
..
}) if self.keyboard && modifiers.is_empty() => {
state.focus_pending = true;
camera = Some(CameraEvent::ToggleFocus);
capture = true;
cancel = true;
@@ -258,12 +271,14 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
capture = true;
}
Key::Named(Named::Tab) => {
state.focus_pending = true;
camera = Some(CameraEvent::Traverse(modifiers.shift()));
capture = true;
}
Key::Named(Named::Escape) => {
state.pan = None;
state.space = false;
state.focus_pending = true;
camera = Some(CameraEvent::Leave);
capture = true;
}
@@ -309,7 +324,9 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_,
shell.request_redraw();
return;
}
if matches!(event, Event::Keyboard(_)) && (!state.focused || !self.keyboard) {
if matches!(event, Event::Keyboard(_))
&& (!state.focused || !self.keyboard || state.pan.is_some() || state.space)
{
return;
}
let child_cursor = if inside {
+31 -1
View File
@@ -66,6 +66,34 @@ impl std::fmt::Display for MessageStyle {
})
}
}
#[cfg(test)]
pub(crate) fn removal_test_point(app: &App, owner: Id, id: Id) -> Point {
let scene = super::Scene::sequence(app.project(), owner, id);
let point = scene
.boxes
.iter()
.rev()
.find(|item| item.id == id && !item.title.is_empty())
.map(|item| Point::new(item.rect.x + 20., item.rect.y + 20.))
.or_else(|| {
scene
.lines
.iter()
.find(|line| line.id == id && !line.label.is_empty())
.map(|line| line.label_at + iced::Vector::new(6., 8.))
})
.expect("rendered scenario target");
let presentation = &app.behavior.scenario.presentation;
Point::ORIGIN
+ presentation.pan
+ iced::Vector::new(point.x, point.y) * presentation.scale(scene.size)
}
pub(crate) fn removal_target(p: &Project, owner: Id, id: Id) -> bool {
super::selected_behavior(p, id) == Some(owner)
&& super::Scene::sequence(p, owner, id).removal_target(owner, id)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Context {
pub document: Id,
@@ -108,6 +136,7 @@ pub enum CanvasEvent {
Back(Context),
Choose(Context, CanvasTool),
Select(Context, Id),
Remove(Context, Id),
Begin {
context: Context,
slot: Slot,
@@ -147,6 +176,7 @@ impl CanvasEvent {
| Self::Back(context)
| Self::Choose(context, _)
| Self::Select(context, _)
| Self::Remove(context, _)
| Self::Begin { context, .. }
| Self::BeginOccurrence { context, .. }
| Self::Reorder { context, .. } => *context,
@@ -1934,7 +1964,7 @@ pub(crate) mod direct_tests {
epoch: 0,
}
}
pub(super) fn placement_fixture() -> (Fixture, Id, Id, Id, Id, Id) {
pub(crate) fn placement_fixture() -> (Fixture, Id, Id, Id, Id, Id) {
let mut f = fixture();
let p = &mut f.project;
let machine = p
@@ -30,6 +30,7 @@ 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),
@@ -70,6 +71,7 @@ impl CanvasEvent {
Self::Scrolled(c, ..)
| Self::Choose(c, ..)
| Self::Select(c, ..)
| Self::Remove(c, ..)
| Self::SelectQualified(c, ..)
| Self::ExpandQualified(c, ..)
| Self::OpenQualified(c, ..)
@@ -507,6 +509,12 @@ impl Input {
}
}
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| {
@@ -635,6 +643,29 @@ impl Graph<'_> {
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),
..
@@ -952,6 +983,55 @@ impl Graph<'_> {
}
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);