Fix inline architecture naming and captured Escape cancellation
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
This commit is contained in:
@@ -1019,6 +1019,7 @@ impl App {
|
||||
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
|
||||
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
|
||||
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
|
||||
architecture_authoring::subscription(self),
|
||||
iced::event::listen_with(|event, status, _| {
|
||||
let iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key,
|
||||
@@ -1076,9 +1077,7 @@ impl App {
|
||||
// navigation and close retain raw input until explicit completion/cancel.
|
||||
if self.architecture.is_some() {
|
||||
if matches!(message, Message::Escape) {
|
||||
self.architecture = None;
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
return Task::none();
|
||||
return self.architecture_event(architecture_authoring::Event::Cancel);
|
||||
}
|
||||
let placing = self.architecture.as_ref().is_some_and(|s| s.saved.placing);
|
||||
if !matches!(
|
||||
|
||||
@@ -3,6 +3,8 @@ use super::*;
|
||||
use iced::widget::{button, column, pick_list, row, scrollable, text, text_input};
|
||||
use serde::{Deserialize, Serialize};
|
||||
mod palette;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub use palette::view as palette;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -21,9 +23,35 @@ pub enum Event {
|
||||
Filter(Filter),
|
||||
ShowPlaced(bool),
|
||||
Page(usize),
|
||||
Edit,
|
||||
CapturedEscape,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub fn subscription(app: &App) -> Subscription<Message> {
|
||||
if app.architecture.as_ref().is_some_and(State::inline) {
|
||||
iced::event::listen_with(captured_escape)
|
||||
} else {
|
||||
Subscription::none()
|
||||
}
|
||||
}
|
||||
|
||||
fn captured_escape(
|
||||
event: iced::Event,
|
||||
status: iced::event::Status,
|
||||
_: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
// A focused Iced text input captures Escape while dropping its focus. Only
|
||||
// inline architecture naming opts into also cancelling that same keypress.
|
||||
(status == iced::event::Status::Captured
|
||||
&& matches!(
|
||||
event,
|
||||
iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
|
||||
..
|
||||
})
|
||||
))
|
||||
.then_some(Message::Architecture(Event::CapturedEscape))
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum Filter {
|
||||
@@ -336,14 +364,20 @@ impl App {
|
||||
}
|
||||
return iced::widget::operation::focus("architecture-name");
|
||||
}
|
||||
Event::CapturedEscape => {
|
||||
if self.architecture.as_ref().is_some_and(State::inline) {
|
||||
return self.architecture_event(Event::Cancel);
|
||||
}
|
||||
}
|
||||
Event::Cancel => {
|
||||
self.architecture = None;
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.status = "Cancelled · Select / move".into();
|
||||
}
|
||||
Event::Name(value) => {
|
||||
if value.len() <= 16384
|
||||
&& let Some(s) = &mut self.architecture
|
||||
&& let (Intent::Create { name, .. } | Intent::Rename { name, .. }) =
|
||||
&& let Intent::Create { name, .. } | Intent::Rename { name, .. } =
|
||||
&mut s.saved.intent
|
||||
{
|
||||
*name = value;
|
||||
@@ -375,13 +409,6 @@ impl App {
|
||||
s.saved.page = page.min(1_000_000);
|
||||
}
|
||||
}
|
||||
Event::Edit => {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
if let Some(s) = &mut self.architecture {
|
||||
s.saved.placing = false;
|
||||
}
|
||||
return iced::widget::operation::focus("architecture-name");
|
||||
}
|
||||
Event::Pick(id) => {
|
||||
if !candidates(self)
|
||||
.iter()
|
||||
@@ -422,7 +449,10 @@ impl App {
|
||||
}
|
||||
}
|
||||
Event::Click(context, point) => {
|
||||
if self.route_context() != Some(context) {
|
||||
if self.route_context() != Some(context)
|
||||
|| !point.x.is_finite()
|
||||
|| !point.y.is_finite()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if self.architecture.as_ref().is_some_and(|s| s.saved.placing) {
|
||||
@@ -460,6 +490,10 @@ impl App {
|
||||
self.status =
|
||||
"The diagram changed. The name is retained; cancel and restart this placement."
|
||||
.into();
|
||||
if let Some(s) = &mut self.architecture {
|
||||
s.saved.placing = false;
|
||||
s.error = Some(self.status.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
let diagram = s.saved.diagram;
|
||||
@@ -553,9 +587,10 @@ pub fn inline_editor(app: &App) -> Option<Element<'_, Message>> {
|
||||
row![
|
||||
iced::widget::Space::new().width(x),
|
||||
iced::widget::opaque(
|
||||
iced::widget::container(name_editor(app))
|
||||
iced::widget::container(scrollable(name_editor(app)))
|
||||
.padding(10)
|
||||
.width(width)
|
||||
.max_height((size.height - y - 8.).max(0.))
|
||||
.style(crate::style::card)
|
||||
)
|
||||
]
|
||||
|
||||
@@ -165,7 +165,7 @@ fn tool<'a>(
|
||||
.into()
|
||||
}
|
||||
pub fn view(app: &App) -> Element<'_, Message> {
|
||||
let mut content = column![tool(
|
||||
let mut content = iced::widget::column![tool(
|
||||
app,
|
||||
Icon::Collapse(app.architecture_palette_expanded),
|
||||
if app.architecture_palette_expanded {
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
use super::*;
|
||||
|
||||
fn setup(layer: Layer) -> App {
|
||||
let mut app = App::with_project(Project::empty());
|
||||
app.layer = layer;
|
||||
app.active_diagram = None;
|
||||
let _ = app.update(Message::AddDiagram);
|
||||
app.zoom = 0.75;
|
||||
app.pan = iced::Vector::new(37., -19.);
|
||||
app
|
||||
}
|
||||
fn send(app: &mut App, event: Event) {
|
||||
let _ = app.update(Message::Architecture(event));
|
||||
}
|
||||
fn create(app: &mut App, kind: Kind, name: &str) -> Id {
|
||||
send(app, Event::Start(kind, Some(Position { x: 120., y: 160. })));
|
||||
send(app, Event::Name(name.into()));
|
||||
send(app, Event::Submit);
|
||||
assert!(app.architecture.is_none(), "{}", app.status);
|
||||
app.selected
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_allocated_work_is_root_owned_and_one_exact_history_change() {
|
||||
for (layer, participant, work) in [
|
||||
(Layer::Oa, Kind::Entity, Kind::Activity),
|
||||
(Layer::Sa, Kind::System, Kind::Function),
|
||||
] {
|
||||
let mut app = setup(layer);
|
||||
let performer = create(&mut app, participant, "Control team");
|
||||
let before = app.document.bundle().clone();
|
||||
let camera = (app.zoom, app.pan);
|
||||
let point = Position { x: 151., y: 221. };
|
||||
send(&mut app, Event::Allocate(work, performer, Some(point)));
|
||||
send(&mut app, Event::Name(" Report 雨 ".into()));
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
send(&mut app, Event::Submit);
|
||||
assert!(app.architecture.is_none(), "{}", app.status);
|
||||
let created = app.selected;
|
||||
let element = &app.project().elements[&created];
|
||||
assert_eq!(element.kind(), work);
|
||||
assert_eq!(element.name, " Report 雨 ");
|
||||
assert_eq!(element.parent, None);
|
||||
assert!(app.project().relations.values().any(|r| {
|
||||
r.kind() == RelationKind::Allocation && r.source() == performer && r.target() == created
|
||||
}));
|
||||
let d = &app.project().diagrams[&app.active_diagram.unwrap()];
|
||||
assert_eq!(d.nodes[&app.selected_node.unwrap()].position, point);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() {
|
||||
use iced::widget::canvas::{Event as CanvasEvent, Program};
|
||||
use iced::{keyboard, mouse};
|
||||
let mut app = setup(Layer::Oa);
|
||||
let element = create(&mut app, Kind::Entity, "Original team");
|
||||
let before = app.document.bundle().clone();
|
||||
let diagrams = app.project().diagrams.clone();
|
||||
let camera = (app.zoom, app.pan);
|
||||
let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(600., 380.));
|
||||
let canvas = crate::diagram::DiagramCanvas {
|
||||
project: app.project(),
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: element,
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
};
|
||||
let mut state = crate::diagram::State::default();
|
||||
let key = CanvasEvent::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::F2),
|
||||
modified_key: keyboard::Key::Named(keyboard::key::Named::F2),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::F2),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
});
|
||||
assert!(
|
||||
canvas
|
||||
.update(&mut state, &key, bounds, mouse::Cursor::Unavailable)
|
||||
.is_none()
|
||||
);
|
||||
let _ = canvas.update(
|
||||
&mut state,
|
||||
&CanvasEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
bounds,
|
||||
mouse::Cursor::Available(iced::Point::new(150., 120.)),
|
||||
);
|
||||
let action = canvas
|
||||
.update(&mut state, &key, bounds, mouse::Cursor::Unavailable)
|
||||
.unwrap();
|
||||
let message = action.into_inner().0.unwrap();
|
||||
let _ = app.update(message);
|
||||
assert!(app.architecture.as_ref().unwrap().inline());
|
||||
assert!(matches!(&app.architecture.as_ref().unwrap().saved.intent,
|
||||
Intent::Rename { element: id, name } if *id == element && name == "Original team"));
|
||||
send(&mut app, Event::Name(" Renamed team 雨 ".into()));
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
send(&mut app, Event::Submit);
|
||||
assert!(app.architecture.is_none(), "{}", app.status);
|
||||
assert_eq!(app.project().elements[&element].name, " Renamed team 雨 ");
|
||||
assert_eq!(app.project().diagrams, diagrams);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_blank_names_and_escape_never_accept_placeholder_or_rename() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let element = create(&mut app, Kind::Actor, "Dispatcher");
|
||||
let before = app.document.bundle().clone();
|
||||
let context = app.route_context().unwrap();
|
||||
for event in [
|
||||
Event::Start(Kind::Activity, Some(Position { x: 220., y: 330. })),
|
||||
Event::Rename(context, element, Position { x: 150., y: 200. }),
|
||||
] {
|
||||
// Every cancel changes the gesture context; use the current context for F2.
|
||||
let event = match event {
|
||||
Event::Rename(_, element, point) => {
|
||||
Event::Rename(app.route_context().unwrap(), element, point)
|
||||
}
|
||||
event => event,
|
||||
};
|
||||
send(&mut app, event);
|
||||
send(&mut app, Event::Name(" \t ".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
assert!(app.architecture.as_ref().unwrap().inline());
|
||||
assert!(app.architecture.as_ref().unwrap().error.is_some());
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_invalid_click_cannot_poison_inline_or_checkpoint_geometry() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
let before = app.document.bundle().clone();
|
||||
let context = app.route_context().unwrap();
|
||||
for point in [
|
||||
Position { x: f32::NAN, y: 0. },
|
||||
Position {
|
||||
x: 0.,
|
||||
y: f32::INFINITY,
|
||||
},
|
||||
] {
|
||||
send(&mut app, Event::Click(context, point));
|
||||
let draft = app.architecture.as_ref().unwrap();
|
||||
assert!(draft.unnamed_tool());
|
||||
assert!(validate_saved(app.project(), &draft.saved).is_ok());
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
send(&mut app, Event::Click(context, Position { x: 25., y: 30. }));
|
||||
assert!(app.architecture.as_ref().unwrap().inline());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_stale_submit_keeps_create_and_rename_visible_and_exact() {
|
||||
for rename in [false, true] {
|
||||
for stale_document in [false, true] {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let element = create(&mut app, Kind::Entity, "Accepted team");
|
||||
let point = Position {
|
||||
x: 151.5,
|
||||
y: 227.25,
|
||||
};
|
||||
let event = if rename {
|
||||
Event::Rename(app.route_context().unwrap(), element, point)
|
||||
} else {
|
||||
Event::Start(Kind::Activity, Some(point))
|
||||
};
|
||||
send(&mut app, event);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Name(" Retained engineering name 雨 ".into()),
|
||||
);
|
||||
// Change only the binding. The accepted model/source must not be
|
||||
// rewritten or a rejected draft silently rebound to the new context.
|
||||
if stale_document {
|
||||
app.architecture.as_mut().unwrap().document = Id::new_v4();
|
||||
} else {
|
||||
app.architecture.as_mut().unwrap().revision = app.document.revision() + 1;
|
||||
}
|
||||
let draft = app.architecture.as_ref().unwrap();
|
||||
let saved = serde_json::to_value(&draft.saved).unwrap();
|
||||
let binding = (draft.document, draft.revision);
|
||||
let model = app.project().clone();
|
||||
let source = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
let camera = (app.zoom, app.pan);
|
||||
|
||||
for _ in 0..2 {
|
||||
send(&mut app, Event::Submit);
|
||||
let draft = app.architecture.as_ref().unwrap();
|
||||
assert!(
|
||||
draft.inline(),
|
||||
"rename={rename}, stale_document={stale_document}"
|
||||
);
|
||||
assert!(inline_editor(&app).is_some());
|
||||
assert_eq!(draft.error.as_deref(), Some(app.status.as_str()));
|
||||
assert!(app.status.contains("cancel and restart"));
|
||||
assert_eq!(serde_json::to_value(&draft.saved).unwrap(), saved);
|
||||
assert_eq!((draft.document, draft.revision), binding);
|
||||
assert_eq!(app.project(), &model);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
}
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!(app.project(), &model);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
let fresh = create(&mut app, Kind::Actor, "Fresh context");
|
||||
assert_eq!(app.project().elements[&fresh].name, "Fresh context");
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &model);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_event(named: iced::keyboard::key::Named) -> iced::Event {
|
||||
use iced::keyboard;
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(named),
|
||||
modified_key: keyboard::Key::Named(named),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn focused_text_input_escape(name: &str) -> Message {
|
||||
use iced::advanced::{Layout, Shell, layout, widget};
|
||||
let mut field: iced::Element<'_, (), iced::Theme, ()> = text_input("Engineering name", name)
|
||||
.id("architecture-name")
|
||||
.on_input(|_| ())
|
||||
.into();
|
||||
let mut tree = widget::Tree::new(field.as_widget());
|
||||
let size = iced::Size::new(300., 40.);
|
||||
let node =
|
||||
field
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree, &(), &layout::Limits::new(iced::Size::ZERO, size));
|
||||
tree.state
|
||||
.downcast_mut::<iced::widget::text_input::State<()>>()
|
||||
.focus();
|
||||
let escape = key_event(iced::keyboard::key::Named::Escape);
|
||||
let mut messages = Vec::new();
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
field.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&escape,
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&(),
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&iced::Rectangle::new(iced::Point::ORIGIN, size),
|
||||
);
|
||||
assert!(
|
||||
shell.is_event_captured(),
|
||||
"Iced consumes focused-input Escape"
|
||||
);
|
||||
assert!(
|
||||
!tree
|
||||
.state
|
||||
.downcast_ref::<iced::widget::text_input::State<()>>()
|
||||
.is_focused()
|
||||
);
|
||||
captured_escape(
|
||||
escape,
|
||||
iced::event::Status::Captured,
|
||||
iced::window::Id::unique(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_first_focused_input_escape_cancels_create_and_rename() {
|
||||
for rename in [false, true] {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let element = create(&mut app, Kind::Actor, "Accepted dispatcher");
|
||||
let model = app.project().clone();
|
||||
let source = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
let camera = (app.zoom, app.pan);
|
||||
let point = Position { x: 439., y: 328.2 };
|
||||
let event = if rename {
|
||||
Event::Rename(app.route_context().unwrap(), element, point)
|
||||
} else {
|
||||
Event::Start(Kind::Actor, Some(point))
|
||||
};
|
||||
send(&mut app, event);
|
||||
let name = " Unaccepted long engineering name 雨 ";
|
||||
send(&mut app, Event::Name(name.into()));
|
||||
assert!(app.architecture.as_ref().unwrap().inline());
|
||||
let message = focused_text_input_escape(name);
|
||||
let _ = app.update(message);
|
||||
assert!(app.architecture.is_none());
|
||||
assert!(inline_editor(&app).is_none());
|
||||
assert_eq!(app.status, "Cancelled · Select / move");
|
||||
assert_eq!(app.project(), &model);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_captured_escape_does_not_cancel_other_controls_or_placement() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
for event in [
|
||||
None,
|
||||
Some(Event::Start(Kind::Entity, None)),
|
||||
Some(Event::Existing(None)),
|
||||
] {
|
||||
send(&mut app, Event::Cancel);
|
||||
if let Some(event) = event {
|
||||
send(&mut app, event);
|
||||
}
|
||||
let saved = app
|
||||
.architecture
|
||||
.as_ref()
|
||||
.map(|s| serde_json::to_value(&s.saved).unwrap());
|
||||
let before = app.document.bundle().clone();
|
||||
let status = app.status.clone();
|
||||
let epoch = app.canvas_epoch;
|
||||
let message = focused_text_input_escape("Other field or existing-element search");
|
||||
let _ = app.update(message);
|
||||
assert_eq!(
|
||||
app.architecture
|
||||
.as_ref()
|
||||
.map(|s| serde_json::to_value(&s.saved).unwrap()),
|
||||
saved
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
assert_eq!(app.status, status);
|
||||
assert_eq!(app.canvas_epoch, epoch);
|
||||
}
|
||||
assert!(
|
||||
captured_escape(
|
||||
key_event(iced::keyboard::key::Named::Escape),
|
||||
iced::event::Status::Ignored,
|
||||
iced::window::Id::unique(),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
captured_escape(
|
||||
key_event(iced::keyboard::key::Named::Enter),
|
||||
iced::event::Status::Captured,
|
||||
iced::window::Id::unique(),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
@@ -621,8 +621,9 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
Key::Named(Named::F2)
|
||||
if self.editable && self.connection.is_none() && modifiers.is_empty() =>
|
||||
{
|
||||
let view = self.scene(state).view;
|
||||
let node = view
|
||||
let scene = self.scene(state);
|
||||
let node = scene
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == self.selected || n.id == self.selected)?;
|
||||
|
||||
@@ -187,8 +187,8 @@ fn article(topic: Topic) -> Article {
|
||||
&[
|
||||
"Choose New in the top bar. Use Project properties to name the model and describe its purpose. Apply changes commits the property draft.",
|
||||
"On Architecture, choose Create architecture view. Architecture · allocated work shows work inside its performer.",
|
||||
"Choose Entity in the diagram's Create toolbar. Enter Rescue coordinator, choose Choose position, then click the diagram. Use Actor for an external participant such as a Vessel in distress. You can also right-click a blank area, choose Create Entity here, enter its name and choose Create here.",
|
||||
"Choose Activity in the same toolbar, name it Assess incident and place it. Select Rescue coordinator and use + Relationship with Allocation to assign Assess incident to it. The allocated-work view then shows the activity inside its performer.",
|
||||
"Expand the diagram's side palette to see tool names. Choose Entity, click its position in the diagram, enter Rescue coordinator and press Enter. Escape cancels the draft. Use Actor for an external participant such as a Vessel in distress. Right-clicking a blank area also offers Create Entity here.",
|
||||
"Right-click Rescue coordinator and choose Add activity. Enter Assess incident and press Enter to create the activity and its performer allocation together. Ordinary Activity creation in the palette leaves work unallocated; use + Relationship with Allocation to assign existing work. The allocated-work view shows work inside its performer.",
|
||||
"Use Add existing to place an element that is already in the model. The initial choices match this view; no search is required. Include already shown offers Locate for elements already placed here.",
|
||||
"Add a second activity for another participant. Select an activity, choose + Relationship, select Interaction and choose the compatible target. Name the exchange after what is conveyed.",
|
||||
"Visit Model checks to review unfinished work. Save all writes the accepted model and applied drafts to your chosen file.",
|
||||
@@ -262,8 +262,8 @@ fn article(topic: Topic) -> Article {
|
||||
"Create and allocate work",
|
||||
&[
|
||||
"Choose the Operational or System perspective. Create participants using Entity/Actor in OA or System/System actor in SA.",
|
||||
"In the diagram's Create toolbar, choose Activity in OA or Function in SA, enter a name and click to place it. For semantic decomposition, use Model explorer, select existing work and use + In selected to create smaller work units.",
|
||||
"To assign work to a participant, use + Relationship with Allocation from the participant to the leaf work. Model explorer also offers + Allocated when a participant and the appropriate work kind are selected.",
|
||||
"In the diagram's side palette, choose Activity in OA or Function in SA, click its position, enter a name and press Enter. Escape cancels without creating an element. Select a card and press F2 to rename it in place. For semantic decomposition, use Model explorer, select existing work and use + In selected to create smaller work units.",
|
||||
"Right-click a participant and choose Add activity or Add function to create new work and its allocation together. To assign existing leaf work, use + Relationship with Allocation from the participant to the work. Model explorer also offers + Allocated when a participant and the appropriate work kind are selected.",
|
||||
"Choose Architecture · allocated work to review responsibilities. Choose Functional dataflow to review the activity/function hierarchy and its exchanges independently.",
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Compact palette implementation verification — 2026-09-10
|
||||
|
||||
Owner: Codex `/root/palette_finish`, assigned by root. Base branch
|
||||
`codex/release1-continuation`, starting HEAD `1797516`. No commit or push is
|
||||
performed by this worker. Root owns native QA, guide changes, integration and the
|
||||
shared release ledgers.
|
||||
|
||||
Read repository AGENTS.md, Gitea issue #1/comments, Wiki Home, open PR inventory
|
||||
(empty), and `harness/release-capella-basic-editing-20260907.md` before editing.
|
||||
Ownership and results are recorded in issue #1.
|
||||
|
||||
## Changes
|
||||
|
||||
* Qualify the Iced `column!` macro in the palette to remove E0659.
|
||||
* Borrow the projected scene when F2 finds its selected node instead of moving
|
||||
its view out of `Arc`, removing E0507.
|
||||
* Remove redundant or-pattern parentheses and the unused old `Edit` event.
|
||||
* Reject non-finite placement coordinates before they can become inline-editor
|
||||
or recovery-draft geometry. Existing context/epoch guards continue rejecting
|
||||
cancelled or stale canvas gestures.
|
||||
* Bound the inline name card to the remaining canvas height and make its content
|
||||
scrollable, so a wrapped performer title or validation error can remain inside
|
||||
the canvas. The card stays in the canvas stack and does not add a toolbar row.
|
||||
|
||||
Four new regression tests live in
|
||||
`crates/oa-desktop/src/app/architecture_authoring/tests.rs`:
|
||||
|
||||
1. OA and SA contextual creation preserves root ownership while adding the
|
||||
allocation and placement atomically; exact source bundle Undo/Redo and camera
|
||||
values are checked.
|
||||
2. A real Iced canvas F2 event requires canvas keyboard focus, dispatches the
|
||||
selected node to inline rename, retains its exact engineering name, and
|
||||
preserves placements/camera and exact source bundle Undo/Redo.
|
||||
3. Blank creation/rename submission and Escape never accept a placeholder or
|
||||
change the accepted source bundle.
|
||||
4. NaN/infinite canvas coordinates cannot poison draft geometry or recovery
|
||||
validation; the next valid click still opens naming.
|
||||
|
||||
## Verification
|
||||
|
||||
All Cargo commands use `.local/toolchain/env.ps1`,
|
||||
`CARGO_BUILD_JOBS=1`, `--locked`, and the worker's exclusive Cargo lease.
|
||||
|
||||
The first repaired build passed 15 existing `architecture_` filtered tests,
|
||||
with 569 filtered out. Log: `.local/palette-focused-initial-20260910.log`.
|
||||
It exposed the unused `Edit` warning, subsequently removed. An initial new-test
|
||||
compile caught an incorrect SourceBundle assertion; it was corrected before
|
||||
the final tests.
|
||||
|
||||
The full current desktop suite passed **588 tests, 0 failures, 0 ignored**:
|
||||
`cargo test --locked -p oa-desktop --bin arcadia-studio -- --test-threads=1`.
|
||||
Compilation/link took 44.58 s and test execution 113.14 s. This includes the
|
||||
four new regressions and existing placement/recovery/connection/transition
|
||||
coverage. Log: `.local/palette-desktop-tests-20260910.log`.
|
||||
|
||||
`cargo build --locked -p oa-desktop --bin arcadia-studio` passed in 2m 03s,
|
||||
including root's updated guide text. Log: `.local/palette-gui-build-20260910.log`.
|
||||
`cargo fmt --all -- --check` passed; its empty success log is
|
||||
`.local/palette-fmt-20260910.log`.
|
||||
|
||||
Built executable:
|
||||
`C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\arcadia-studio.exe`
|
||||
(109,516,288 bytes), modified `2026-09-10T17:42:00.6365121Z`.
|
||||
SHA-256: `1B47C096624BB1101BEC8B18E63CD601A259DFF8C1BEC24581C31C6B511F9411`.
|
||||
Root received this path/hash for an isolated native-QA staging copy.
|
||||
|
||||
`cargo clippy --locked -p oa-desktop --all-targets -- -D warnings` passed in
|
||||
47.56 s. Log: `.local/palette-clippy-20260910.log`.
|
||||
|
||||
All assigned gates are complete. The worker released its Cargo lease and source
|
||||
freeze to root; subsequent workers must obtain the next lease from root. Source
|
||||
and report changes remain uncommitted for root's integration. No native UI was
|
||||
operated by this worker.
|
||||
|
||||
## Scope and remaining acceptance
|
||||
|
||||
Automated application/canvas tests establish state transitions and exact history;
|
||||
they do not establish native focus, visual compactness or pointer ergonomics.
|
||||
Root must inspect the newly built executable at wide/minimum sizes, especially
|
||||
bottom/right-edge naming, long names, wrapped errors, expanded palette labels,
|
||||
Enter/Escape, F2, existing insertion/Locate and contextual allocation.
|
||||
|
||||
Canvas origin must remain stable through arming, naming, validation, commit and
|
||||
cancel; only the explicit palette width toggle may resize the canvas. Continue
|
||||
the independent basic-editing oracle, Save/reopen/recovery and OA/SA transition
|
||||
destination checks. No Windows release acceptance or Linux runtime acceptance
|
||||
is inferred from this implementation report.
|
||||
|
||||
## Follow-up: stale submission rejection
|
||||
|
||||
Independent local review and the separate Codex-956atf host's issue #1 comment
|
||||
#12 identified a remaining stale-submit defect after the first verification:
|
||||
Submit set `placing=true`, then a stale document/revision guard returned with
|
||||
only a status string. That hid the retained inline name card. Earlier passing
|
||||
gates above did not cover this rejected-submit path.
|
||||
|
||||
Codex-9e8r6t `/root/palette_finish` resumed the assigned architecture scope and
|
||||
sole Cargo lease. Guard rejection now restores `placing=false` and copies the
|
||||
explanation into the card's error, without rebinding its document/revision or
|
||||
changing its name, point or intent.
|
||||
|
||||
A new regression covers Create/Rename × stale document/revision (four cases),
|
||||
repeated Submit without rebinding, actual inline-view presence, exact serialized
|
||||
saved draft, accepted Project/source/revision/camera preservation, Escape, and
|
||||
successful fresh creation with exact Undo after cancellation. The focused
|
||||
`architecture_` gate passed 20 tests, 569 filtered out, in 2.22 s after 48.63 s
|
||||
compilation. Log: `.local/palette-stale-submit-tests-20260910.log`.
|
||||
|
||||
Root's native `1B47C096…` check then found that the first Escape was consumed by
|
||||
the focused Iced name field and only unfocused it. The original global keyboard
|
||||
subscription intentionally ignores captured Escape; that policy remains intact.
|
||||
A separate subscription is now present only while architecture naming is inline,
|
||||
and dispatches a narrowly rechecked `CapturedEscape` event. The Cancel handler
|
||||
also restores `Cancelled · Select / move` status; ordinary architecture Escape
|
||||
delegates to that handler. The only app.rs edits are this subscription entry and
|
||||
the pre-existing architecture Escape guard delegation.
|
||||
|
||||
Two additional regressions exercise actual Iced TextInput update/capture followed
|
||||
by the subscription mapper and App update: first-keypress Create/Rename cancel
|
||||
preserves exact accepted model/source/revision/camera, while absent architecture,
|
||||
armed placement and the existing-element picker do not react to captured Escape.
|
||||
Ignored Escape and captured Enter are not mapped by the new subscription.
|
||||
|
||||
The final `architecture_` gate passed **22 tests**, 569 filtered out, in 4.12 s
|
||||
after 2m 19s compilation. Log: `.local/palette-escape-tests-20260910.log`.
|
||||
The same locked desktop build command passed in 1m 12s; log:
|
||||
`.local/palette-candidate2-build-20260910.log`. Workspace formatting check passed;
|
||||
log: `.local/palette-candidate2-fmt-20260910.log`.
|
||||
|
||||
Second candidate at the same local build path: 109,518,848 bytes, modified
|
||||
`2026-09-10T17:59:06.6655664Z`.
|
||||
SHA-256: `E6D6B1D975A11F242D288039E8F07D20E5474EA9C7CFCDE93F1318046EA548D4`.
|
||||
Root received this identity for a new staging copy. Strict desktop Clippy
|
||||
(`--locked -p oa-desktop --all-targets -- -D warnings`) passed in 1m 07s;
|
||||
log: `.local/palette-candidate2-clippy-20260910.log`. Cargo lease is released to
|
||||
root again. No worker-owned Cargo process remains.
|
||||
|
||||
Root's existing native stage remains the earlier `1B47C096…` binary and is not
|
||||
overwritten. The full integrated suite is deferred until the other-host behavior
|
||||
and forms changes are integrated; the earlier 588-test scope is preserved above.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Independent palette review — 2026-09-10
|
||||
|
||||
Reviewer: **Codex-9e8r6t /root/behavior_audit**. Root requested this bounded read-only review while the separate-host scenario patch is being coordinated. Reviewed the current uncommitted architecture delta over `1797516`, its new/existing tests, guide wording, [implementation evidence](release-palette-implementation-20260910.md), and [original native acceptance](release-capella-basic-editing-20260907.md). Read Gitea issue #1, including Codex-956atf's stale-submission finding in comment #12. No production files, Cargo commands or native input were used in this review.
|
||||
|
||||
## Confirmed finding
|
||||
|
||||
**P2 — stale submission hides the retained inline name.**
|
||||
|
||||
`crates/oa-desktop/src/app/architecture_authoring.rs:400–416` validates nonempty input, then sets `saved.placing = true` before calling `commit_architecture`. The commit guard at `447–460` rejects stale document/revision/diagram, dirty source/properties/behavior or invalid point, but it only sets the status string and returns. It does not restore `placing = false` or set the draft error. `State::inline` at `95–99` requires `!placing`, and `inline_editor` at `540–541` filters on that predicate.
|
||||
|
||||
Consequently the model remains unchanged and the bytes of the name remain in memory, but the name editor disappears on this rejection. The status says the name is retained while asking the user to cancel/restart. The old `Edit` action was removed in this delta; no direct repair action restores the editor. A later canvas click can reopen naming, but cannot repair the stale document/revision binding. This is a defect in the stale-context recovery path, **not an observed ordinary native workflow failure or accepted-model corruption**. Most normal navigation/model edits are already blocked during naming by `app.rs:1075–1097`.
|
||||
|
||||
**Minimal correction:** on the guarded rejection, retain all raw fields and set `saved.placing = false` plus `error = Some(status.clone())`, as the actual execution-failure branches already do at `474–477` and `534–536`. Keep rejection of the stale commit; do not silently refresh its document/revision or accept it against another model. Ensure the retained editor can be read/copied/cancelled. This mirrors the separate-host finding but was independently confirmed in our current implementation.
|
||||
|
||||
**Focused falsifier to add:** for both named Create-at-point and F2 Rename, capture the raw name, accepted bundle and camera; invalidate the saved document/revision binding in the test fixture; send `Event::Submit`. Assert the bundle and camera are exact, no new history is created, the same raw name/point remains, `inline()` is true, and `error` is populated. Repeat Submit without repair and assert it still rejects. Cancel must remove only the draft. Restore ordinary naming afterward to ensure the failed draft does not poison the next operation. The existing stale test changes a **Click event**'s revision (`architecture_authoring_tests.rs:65–86`), so it does not cover this Submit path. The four new tests cover blank Submit and invalid Click, not stale Submit.
|
||||
|
||||
## Areas reviewed without another confirmed source defect
|
||||
|
||||
- The new F2 test drives a real Iced canvas event, requires canvas focus and verifies exact source/history plus unchanged placements/camera. The production Arc borrow fix is correct and does not alter selection identity.
|
||||
- Named OA/SA allocated work is root-owned with a distinct allocation and placement in one Batch. New tests compare exact source bundles before/after Undo/Redo; no semantic-parent/allocation conflation was found.
|
||||
- Nonfinite Click rejection precedes saved geometry, and State restoration also validates point finiteness, compatible type/performer and accepted fingerprint. Existing checkpoint tests retain raw names, phase and point with a fresh process binding. These safeguards should stay intact.
|
||||
- Name length is bounded, whitespace-only Submit keeps the editor/error, and Escape cancels without accepting a placeholder. Choosing another creation tool is allowed only while the first tool is unnamed; named inputs are protected by the App gate.
|
||||
- The new card height bound and scrollable wrapper directly address a long title/error overflowing the canvas. Source review does not prove native edge placement, focus, scrolling or expanded-label readability. There is no new measured layout assertion for this wrapper; retain root's minimum/wide native acceptance.
|
||||
- Current guide text teaches side-palette creation, point then inline name/Enter, Escape, F2, explicit allocated work and palette connections. Those statements match the inspected architecture implementation. Scenario guide labels will need synchronization after the separate clicked-slot/layout work; that is outside this palette review.
|
||||
|
||||
## Validation boundary
|
||||
|
||||
The upstream palette report records desktop588, GUI build, formatting and strict desktop Clippy. This reviewer did not rerun them. Root reports actual tool→point→inline→Enter with stable screen origin and one exact source Undo; that evidence belongs in root's native report. It does not exercise stale submission, long names at the lower/right edge, existing insertion/Locate, recovered inline focus or all OA/SA allocation cases.
|
||||
|
||||
Fix and run the focused stale-submission falsifier before claiming that rejection paths retain a usable inline editor. No other blocking finding was established in this bounded review. Scenario production remains unchanged pending root's source-coordination clearance.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Palette native acceptance — 2026-09-10
|
||||
|
||||
Codex-9e8r6t `/root`, actual Windows Computer Use through the bundled `@oai/sky`
|
||||
API. This is ongoing acceptance, not a release verdict.
|
||||
|
||||
## Candidate and isolation
|
||||
|
||||
Initial binary SHA256
|
||||
`1B47C096624BB1101BEC8B18E63CD601A259DFF8C1BEC24581C31C6B511F9411`,
|
||||
109,516,288 bytes, staged with the exact LLVM-MinGW libunwind DLL and examples
|
||||
in `.local/native-qa/palette-20260910-v1/bin`. Process4852 started at
|
||||
17:42:58UTC with fresh task-local APPDATA/LOCALAPPDATA and a clean Windows PATH.
|
||||
`process.json` records hashes, paths and process identity; stdout/stderr are
|
||||
retained there. No pre-existing Arcadia GUI was touched. Native window525310
|
||||
was selected from actual returned windows.
|
||||
|
||||
The frozen independent fixture was copied to the run's `work` directory.
|
||||
Checker self-check and baseline passed before UI work using the local toolchain
|
||||
runtime on PATH. The native Open dialog loaded this copy. UIA value setting in
|
||||
the Windows file dialog was unavailable (`CacheRequest`,0x80070057); actual
|
||||
focused filename typing succeeded. This was a test-tool issue, not an app error.
|
||||
|
||||
## Observed and verified
|
||||
|
||||
- At1442×932 outer dimensions, expanding the palette reveals Entity/Actor,
|
||||
Activity, compatible connections and Add existing. Incompatible drawing tools
|
||||
explain their missing source. Expansion changes canvas width only.
|
||||
- Entity tool → point(381,435) → focused inline field → `Rescue team` → Enter
|
||||
worked. Canvas origin remained at approximately(261,315) throughout all phases;
|
||||
the accepted card appeared at the chosen screen point. No placeholder model
|
||||
element was accepted before Enter. Opening a project had retained a camera
|
||||
pan, so this screen point was not world(120,120).
|
||||
- Ctrl+S succeeded first submission. The initial fixed-coordinate entity oracle
|
||||
intentionally failed because the operator had not yet set its required world
|
||||
position: saved(90,90.20001220703125), expected(120,120). This is not evidence
|
||||
of a click-transform defect: the observed screen placement matched the click.
|
||||
The complete click-state bundle is preserved in `checkpoint-entity-click`.
|
||||
- One Ctrl+Z removed the whole creation. Ctrl+S then passed the complete baseline
|
||||
model and both exact source files. Preserved in `checkpoint-entity-undo`.
|
||||
Ctrl+Y restored the same Entity/placement identity visually.
|
||||
- Through the actual node context menu's Placement properties, the operator
|
||||
explicitly set X=120,Y=120 and applied. Ctrl+S passed the independent complete
|
||||
entity model and all **three** source files. Preserved in `checkpoint-entity`.
|
||||
The existing oracle was not changed to fit the observed result.
|
||||
- Native border resizing reached1102×732 outer dimensions, corresponding to
|
||||
the configured1100×700 client minimum. Both expanded156px and compact36px
|
||||
palettes remained usable. The canvas origin stayed stable; the existing
|
||||
entity retained its world coordinates and visible camera position.
|
||||
- Compact Actor tool → lower-right canvas point(730,673) opened the inline
|
||||
editor clamped wholly within the available canvas. A long Unicode name was
|
||||
horizontally editable without card overflow; Create/Cancel remained visible.
|
||||
|
||||
## Confirmed defect and next verification
|
||||
|
||||
**Escape from the focused inline text input does not cancel on the first key.**
|
||||
It unfocuses the input while retaining the inline card and pending title state.
|
||||
A separate screenshot observation confirmed the result. `app.rs`'s keyboard
|
||||
subscription discards captured Escape, and Iced's text input captures it.
|
||||
Palette worker is implementing narrowly scoped inline cancellation, preserving
|
||||
other widgets' captured-Escape behavior. This was missed by tests that directly
|
||||
sent the Cancel event. Stale-Submit visibility was independently found in source
|
||||
and is being fixed in the same next candidate.
|
||||
|
||||
Explicit Cancel subsequently closed the draft; Ctrl+S then passed the complete
|
||||
entity model and all three exact sources (`checkpoint-cancelled`). The captured
|
||||
pending recovery snapshot is separately preserved in
|
||||
`checkpoint-long-draft-recovery`: exact Actor name, point(439,328.2), placing=false.
|
||||
It is a retained checkpoint for a recovery test, not a claimed crash test.
|
||||
The Cancel footer still gave the old positioning instruction; that feedback is
|
||||
also corrected in the next candidate. `escape-retains-inline.png` records the
|
||||
actual first-Escape failure.
|
||||
|
||||
## Scenario layout baseline
|
||||
|
||||
Opened the staged teaching example from Home at the same1102×732 outer size.
|
||||
Its process view uses most of the center pane for the graph. Selected Scenario
|
||||
`Repair request and measured reply`: its heading/tools pushed the idle diagram
|
||||
top to approximately483px, leaving only its upper part visible. Choosing
|
||||
`+ Message` shrank its diagram to approximately110px: only the Controller and
|
||||
Technician headings fit; the exchanges were below the viewport. The form also
|
||||
had a small independently scrolling field area. Saved evidence:
|
||||
`scenario-minimum-before.png` and `scenario-minimum-pending-before.png`.
|
||||
This confirms the source audit's scenario workspace problem. Explicit Cancel
|
||||
restored the clean teaching model; no semantic edit was committed.
|
||||
|
||||
The first candidate closed cleanly through its native title-bar button. The
|
||||
subsequent process check found process4852 absent and its stderr log empty.
|
||||
Next: verify corrected first Escape and restored checkpoint focus, then F2,
|
||||
named/existing placement, allocation, connection, transitions and history/reopen.
|
||||
Minimum-size observations alone do not complete those remaining workflows.
|
||||
|
||||
## Candidate 2: focused Escape and recovery
|
||||
|
||||
The isolated `palette-20260910-v2` stage contains the rebuilt candidate with
|
||||
SHA-256 `E6D6B1D975A11F242D288039E8F07D20E5474EA9C7CFCDE93F1318046EA548D4`.
|
||||
Its `process.json` records process 648, the executable and runtime DLL hashes,
|
||||
and its separate profile/work directories. The candidate 1 stage was preserved.
|
||||
|
||||
Home's Recover action restored the retained long Actor-name checkpoint. Both
|
||||
the complete accepted bundle and serialized architecture draft matched the
|
||||
preserved checkpoint; the comparison is retained in `checkpoint-restored-recovery`.
|
||||
The inline card was visible with its full name and position, but recovery did
|
||||
not automatically focus its name field (`recovered-inline-before-focus.png`).
|
||||
After clicking that field, **one Escape cancelled the draft**, returned the
|
||||
palette to Select, and showed `Cancelled · Select / move`. This verifies the
|
||||
actual captured-key fix, not merely a directly injected Cancel event.
|
||||
|
||||
Ctrl+S opened Save As for the recovered copy. Submitting the full path to
|
||||
`recovered-work/project.arcadia` saved successfully on the first submission.
|
||||
The independent checker passed the complete Entity model and all three exact
|
||||
source files; `checkpoint-recovered-save` preserves the result. A read-only
|
||||
repeat after the desktop-control stop also passed the same oracle.
|
||||
|
||||
The user stopped Computer Use with physical Escape during the next observation.
|
||||
No rename action was performed by that stopped call. Native control is paused;
|
||||
F2 rename, the remaining complete basic-editing sequence, transitions and final
|
||||
reopen/runtime acceptance remain pending. Source integration and packaging
|
||||
continue. Recovery autofocus remains a small confirmed usability follow-up.
|
||||
Reference in New Issue
Block a user