experiment: isolate read-only viewport comparison runner

This commit is contained in:
2026-10-01 20:54:07 -07:00
parent 9ded0ff215
commit 3878951262
5 changed files with 1563 additions and 7 deletions
+744
View File
@@ -0,0 +1,744 @@
//! Private read-only optical comparison. Normal startup never constructs this runner.
mod inert;
#[cfg(test)]
mod tests;
use crate::{app::App, diagram::DiagramCanvas, style};
use iced::{
Element, Fill, Rectangle, Renderer, Size, Task, Theme, Vector, mouse,
widget::{self, button, canvas, column, container, row, text},
};
use oa_core::{Id, Kind, Layer, Project, projection, routing};
use serde_json::{Value, json};
use std::{
cell::RefCell,
fs,
io::Write,
path::{Path, PathBuf},
rc::Rc,
};
type Result<T> = std::result::Result<T, String>;
type Mailbox = Rc<RefCell<Option<DrawStamp>>>;
pub(crate) fn requested(args: &[String]) -> bool {
args.get(1).is_some_and(|arg| arg == "--focus-optical")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Variant {
Baseline,
Focus,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Camera {
zoom: f32,
pan: Vector,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct DrawStamp {
project: Id,
diagram: Id,
variant: Variant,
generation: u64,
viewport: Size,
camera: Camera,
}
#[derive(Debug, Clone)]
enum Message {
Variant(Variant),
Fit,
Capture,
Measured(u64, Size),
Drawn(DrawStamp),
Header(u64, Size),
Opened(iced::window::Id, Size),
WindowSize(Size),
Scale(f32),
Close(iced::window::Id),
UnexpectedPassive(String),
}
#[derive(Clone)]
struct Frozen {
diagram: Id,
project: Project,
projected: projection::ProjectedView,
routed: routing::RoutedView,
svg: String,
report: Value,
}
impl Frozen {
fn new(project: Project, diagram: Id) -> Result<Self> {
let Some(saved) = project.diagrams.get(&diagram) else {
return Err("The named diagram does not exist.".into());
};
if !saved.projection.is_architecture() || saved.projection.scenario().is_some() {
return Err("The optical comparison accepts saved Architecture diagrams only.".into());
}
let projected = projection::project_diagram(&project, saved);
let routed = routing::route_diagram(&project, saved, &projected);
let svg = crate::diagram::export_svg(&project, diagram).map_err(|e| e.to_string())?;
let report = scene_report(&project, diagram, &projected, &routed);
Ok(Self {
diagram,
project,
projected,
routed,
svg,
report,
})
}
fn check(&self, app: &App, diagram: Id) -> Result<()> {
if app.project() != &self.project {
return Err("Frozen complete Project changed.".into());
}
let saved = &app.project().diagrams[&diagram];
let projected =
projection::project_diagram_with_index(app.project(), saved, app.document.view_index());
let routed = routing::route_diagram(app.project(), saved, &projected);
let svg = crate::diagram::export_svg(app.project(), diagram).map_err(|e| e.to_string())?;
if projected != self.projected || routed != self.routed || svg != self.svg {
return Err("Frozen projection, complete routed geometry or SVG changed.".into());
}
if app.active_diagram != Some(diagram)
|| app.layer != saved.layer
|| app.page != crate::app::Page::Diagram
|| app.selected != app.project().id
|| app.diagram_selection.is_some()
|| app.connection.is_some()
|| app.architecture.is_some()
{
return Err("Optical presentation fixture is no longer neutral.".into());
}
if app.document.dirty()
|| app.document.can_undo()
|| app.document.can_redo()
|| app.draft_dirty()
|| app.source_dirty()
|| app.context_help
|| app.busy
|| app.pending.is_some()
|| app.model_menu.is_some()
|| app.file_prompt.is_some()
|| app.diagram_navigation_focus.is_some()
|| app.diagram_usages.is_some()
{
return Err(
"Optical fixture acquired draft, history, modal or navigation state.".into(),
);
}
Ok(())
}
}
fn scene_report(
project: &Project,
diagram: Id,
projected: &projection::ProjectedView,
routed: &routing::RoutedView,
) -> Value {
let label = |l: &routing::EdgeLabelGeometry| {
json!({
"text": l.text, "fraction": l.fraction, "anchor": l.anchor, "center": l.center,
"bounds": {"position": l.bounds.position, "size": l.bounds.size}
})
};
let edges: Vec<_> = routed
.edges
.values()
.map(|edge| {
let ends = project
.relations
.get(&edge.id)
.map(|relation| {
routing::association_end_geometry(relation, edge).iter().map(|end| json!({
"source_end": end.source_end, "label": label(&end.label), "tip": end.tip,
"inward": end.inward, "navigable": end.navigable
})).collect::<Vec<_>>()
})
.unwrap_or_default();
json!({"id": edge.id, "points": edge.points, "label": label(&edge.label),
"pins": edge.pins, "pin_fractions": edge.pin_fractions,
"warnings": edge.warnings.iter().map(|w| format!("{w:?}")).collect::<Vec<_>>(),
"custom_suspended": edge.custom_suspended, "association_ends": ends})
})
.collect();
json!({"diagram": diagram, "projected": projected,
"routed": {"bounds": {"position": routed.bounds.position, "size": routed.bounds.size}, "edges": edges},
"complete_routed_debug": format!("{routed:#?}"),
"debug_scope": "Includes private derived routing fields; complete RoutedView PartialEq is also checked on every capture."})
}
fn fit(bounds: routing::RoutingBounds, empty: bool, viewport: Size) -> Camera {
if empty {
return Camera {
zoom: 1.,
pan: Vector::new(30., 30.),
};
}
let width = (viewport.width - 48.).max(1.);
let height = (viewport.height - 48.).max(1.);
// Preserve production f32 operation order, including origin addition/subtraction.
let left = bounds.position.x;
let top = bounds.position.y;
let right = left + bounds.size.width;
let bottom = top + bounds.size.height;
let zoom = (width / (right - left).max(1.))
.min(height / (bottom - top).max(1.))
.clamp(0.0001, 1.5);
Camera {
zoom,
pan: Vector::new(
24. + (width - (right - left) * zoom) / 2. - left * zoom,
24. + (height - (bottom - top) * zoom) / 2. - top * zoom,
),
}
}
fn valid_size(size: Size) -> bool {
size.width.is_finite() && size.height.is_finite() && size.width > 0. && size.height > 0.
}
struct LayoutState {
variant: Variant,
generation: u64,
expected: Option<DrawStamp>,
acknowledged: Option<DrawStamp>,
}
impl Default for LayoutState {
fn default() -> Self {
Self {
variant: Variant::Baseline,
generation: 0,
expected: None,
acknowledged: None,
}
}
}
impl LayoutState {
fn invalidate(&mut self) {
self.generation = self.generation.wrapping_add(1);
self.expected = None;
self.acknowledged = None;
}
fn measured(&mut self, generation: u64, viewport: Size, frozen: &Frozen) -> bool {
if generation != self.generation
|| !valid_size(viewport)
|| self
.expected
.is_some_and(|stamp| stamp.viewport == viewport)
{
return false;
}
// A genuine canvas resize can happen without a native WindowResized.
if self.expected.is_some() {
self.invalidate();
}
self.expected = Some(DrawStamp {
project: frozen.project.id,
diagram: frozen.diagram,
variant: self.variant,
generation: self.generation,
viewport,
camera: fit(
frozen.routed.bounds,
frozen.projected.nodes.is_empty(),
viewport,
),
});
self.acknowledged = None;
true
}
fn drawn(&mut self, stamp: DrawStamp) -> bool {
if Some(stamp) != self.expected || self.acknowledged == Some(stamp) {
return false;
}
self.acknowledged = Some(stamp);
true
}
fn ready(&self) -> bool {
self.expected.is_some() && self.expected == self.acknowledged
}
}
/// No update override: no interactive canvas state can ever be mutated here.
struct OpticalCanvas<'a> {
program: DiagramCanvas<'a>,
stamp: Option<DrawStamp>,
drawn: Mailbox,
}
impl canvas::Program<crate::app::Message> for OpticalCanvas<'_> {
type State = crate::diagram::State;
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let geometry = <DiagramCanvas<'_> as canvas::Program<crate::app::Message>>::draw(
&self.program,
state,
renderer,
theme,
bounds,
mouse::Cursor::Unavailable,
);
if let Some(stamp) = self.stamp.filter(|s| {
s.viewport == bounds.size()
&& s.camera.zoom == self.program.zoom
&& s.camera.pan == self.program.pan
}) {
*self.drawn.borrow_mut() = Some(stamp);
}
geometry
}
}
struct Runner {
fixture: App,
frozen: Frozen,
diagram: Id,
input_path: PathBuf,
input: Vec<u8>,
output: PathBuf,
layout: LayoutState,
drawn: Mailbox,
header: Option<(u64, Size)>,
scale_factor: Option<f32>,
capture: u32,
error: Option<String>,
}
fn fixture(project: Project, diagram: Id) -> Result<App> {
// Preflight the same conversion used by with_project so invalid input is a visible error.
let document =
crate::document::Document::from_project(project.clone(), crate::source_workspace::LIMITS)
.map_err(|e| e.to_string())?;
if document.project() != &project {
return Err("Document admission changed the complete Project.".into());
}
let layer = project.diagrams[&diagram].layer;
let mut app = App::with_project(project);
app.active_diagram = Some(diagram);
app.layer = layer;
app.page = crate::app::Page::Diagram;
app.explorer_index = crate::explorer::Index::new(app.project(), layer);
app.create_kind = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
app.architecture = None;
app.architecture_palette_expanded = false;
app.connection = None;
app.selected = app.project().id;
app.selected_node = None;
app.diagram_selection = None;
app.selected_attachment = None;
app.selected_bend = None;
app.draft = crate::forms::Draft::from_project(app.project(), app.selected);
app.diagram_navigation_focus = None;
app.diagram_usages = None;
app.status =
"READ-ONLY OPTICAL EXPERIMENT · Waiting for measured canvas and draw acknowledgment".into();
Ok(app)
}
impl Runner {
fn invalidate(&mut self) {
self.layout.invalidate();
self.header = None;
*self.drawn.borrow_mut() = None;
self.fixture.status =
"READ-ONLY · Waiting for measured canvas and draw acknowledgment".into();
}
fn update(&mut self, message: Message) -> Task<Message> {
// A fault is terminal for this evidence session. Queued redraw/capture
// messages cannot restore readiness or write another successful capture.
if self.error.is_some() && !matches!(&message, Message::Close(_)) {
return Task::none();
}
match message {
Message::Variant(variant) if variant != self.layout.variant => {
self.layout.variant = variant;
self.invalidate();
}
Message::Fit => self.invalidate(),
Message::Measured(generation, size) => {
if self.layout.measured(generation, size, &self.frozen) {
let stamp = self.layout.expected.unwrap();
self.fixture.zoom = stamp.camera.zoom;
self.fixture.pan = stamp.camera.pan;
*self.drawn.borrow_mut() = None;
self.fixture.status =
"READ-ONLY · Fit measured; waiting for matching canvas draw".into();
}
}
Message::Drawn(stamp) => {
if self.layout.drawn(stamp) {
self.fixture.status = format!(
"READ-ONLY · {:?} ready · {:.1} × {:.1} · Fit {:.5} · Capture records draw acknowledgment, not GPU completion",
stamp.variant,
stamp.viewport.width,
stamp.viewport.height,
stamp.camera.zoom
);
}
}
Message::Header(generation, size)
if generation == self.layout.generation && valid_size(size) =>
{
self.header = Some((generation, size));
}
Message::WindowSize(size) if valid_size(size) && size != self.fixture.window_size => {
self.fixture.window_size = size;
self.invalidate();
}
Message::Opened(id, size) => {
if valid_size(size) && size != self.fixture.window_size {
self.fixture.window_size = size;
self.invalidate();
}
return iced::window::scale_factor(id).map(Message::Scale);
}
Message::Scale(scale)
if scale.is_finite() && scale > 0. && self.scale_factor != Some(scale) =>
{
self.scale_factor = Some(scale);
self.invalidate();
}
Message::Capture => {
if !self.layout.ready()
|| *self.drawn.borrow() != self.layout.expected
|| self.header.is_none_or(|(g, _)| g != self.layout.generation)
|| self.scale_factor.is_none()
{
self.fixture.status =
"READ-ONLY · Capture refused: waiting for matching fitted draw".into();
} else if let Err(error) = self.capture() {
self.fail(error);
}
}
Message::UnexpectedPassive(reason) => self.fail(reason),
Message::Close(id) => {
let conservation = self.check_frozen();
if let Err(error) = &conservation {
self.fail(error.clone());
}
let succeeded = conservation.is_ok() && self.error.is_none();
let result = write_json(
&self.output.join("finished.json"),
&json!({
"status": if succeeded { "PASS" } else { "FAIL" },
"complete_project_scene_svg_and_input_unchanged": conservation.is_ok(),
"captures": self.capture, "error": self.error,
"scope": "Optical runner close; failed conservation or an earlier fault cannot produce PASS. No readability acceptance."
}),
);
if let Err(error) = result {
self.fail(error);
}
// A changed fixture or occupied receipt must never trap the
// user. Existing evidence is preserved; failures remain in stderr.
return iced::window::close(id);
}
_ => {}
}
Task::none()
}
fn fail(&mut self, error: String) {
eprintln!("Focus optical error: {error}");
self.error = Some(error);
self.layout.acknowledged = None;
}
fn check_frozen(&self) -> Result<()> {
self.frozen.check(&self.fixture, self.diagram)?;
if fs::read(&self.input_path).map_err(|e| e.to_string())? != self.input {
return Err("The original fixture bytes changed during the comparison.".into());
}
Ok(())
}
fn capture(&mut self) -> Result<()> {
if let Some(error) = &self.error {
return Err(format!(
"This optical evidence session has stopped: {error}"
));
}
if !self.layout.ready()
|| *self.drawn.borrow() != self.layout.expected
|| self.header.is_none_or(|(g, _)| g != self.layout.generation)
|| self.scale_factor.is_none()
{
return Err(
"Capture is pending a matching measured viewport, header, scale factor and draw."
.into(),
);
}
self.check_frozen()?;
let stamp = self.layout.expected.unwrap();
let next = self.capture + 1;
// One fresh directory makes partial failures visible without overwriting any capture.
let directory = self.output.join(format!("capture-{next:03}"));
fs::create_dir(&directory).map_err(|e| e.to_string())?;
write_json(
&directory.join("measurement.json"),
&json!({
"variant": format!("{:?}", stamp.variant), "generation": stamp.generation,
"viewport": {"width": stamp.viewport.width, "height": stamp.viewport.height},
"zoom": stamp.camera.zoom, "pan": {"x": stamp.camera.pan.x, "y": stamp.camera.pan.y},
"logical_client": {"width": self.fixture.window_size.width, "height": self.fixture.window_size.height},
"scale_factor": self.scale_factor, "scale_factor_source": "Iced window scale_factor task / Rescaled event",
"measured_header_height": self.header.unwrap().1.height,
"baseline_deviation": "Only shared header action group is replaced; original logo/project typography/padding and body chrome retained.",
"draw_acknowledged": true, "gpu_completion_claimed": false,
"screenshot": "Separate native capture required to establish displayed output.",
"selection": "project (no projected object)", "hover": false,
"palette_expanded": false, "diagram": self.diagram,
"complete_project_projection_routed_svg_equal": true,
"scope": "Fit-only read-only optical measurement; no reading or editing acceptance."
}),
)?;
write_json(
&directory.join("project.json"),
&serde_json::to_value(&self.frozen.project).map_err(|e| e.to_string())?,
)?;
write_json(&directory.join("scene.json"), &self.frozen.report)?;
write_new(&directory.join("diagram.svg"), self.frozen.svg.as_bytes())?;
self.capture = next;
self.fixture.status = format!(
"READ-ONLY · Captured {next:03} · {:?} · native screenshot still required",
stamp.variant
);
Ok(())
}
fn view(&self) -> Element<'_, Message> {
let generation = self.layout.generation;
let ready = self.layout.ready()
&& self.header.is_some_and(|(g, _)| g == generation)
&& self.scale_factor.is_some()
&& self.error.is_none();
let controls = row![
text("READ ONLY · EXPERIMENT").size(11),
button(text("Baseline").size(12))
.padding([7, 9])
.on_press(Message::Variant(Variant::Baseline)),
button(text("Focus").size(12))
.padding([7, 9])
.on_press(Message::Variant(Variant::Focus)),
button(text("Fit").size(12))
.padding([7, 9])
.on_press(Message::Fit),
button(text("Capture").size(12))
.padding([7, 9])
.on_press_maybe(ready.then_some(Message::Capture)),
]
.spacing(6)
.align_y(iced::Center);
let header: Element<'_, Message> = if self.layout.variant == Variant::Baseline {
crate::views::header(&self.fixture, controls.into())
} else {
container(
row![
text(crate::views::short(
&self.frozen.project.diagrams[&self.diagram].name,
38
))
.size(14),
widget::Space::new().width(Fill),
controls
]
.spacing(10)
.align_y(iced::Center),
)
.padding([6, 12])
.height(48)
.style(style::panel)
.into()
};
let header = widget::sensor(header)
.key(generation)
.on_show(move |size| Message::Header(generation, size))
.on_resize(move |size| Message::Header(generation, size));
if let Some(error) = &self.error {
return column![header, container(column![text("Optical comparison stopped").size(22), text(error).size(16),
text("No further capture is accepted. Inspect logs and preserve this failed session.").size(14)]
.spacing(16)).padding(24)].into();
}
let program = DiagramCanvas {
selection: None,
allocation: None,
project: self.fixture.project(),
view_index: Some(self.fixture.document.view_index()),
diagram: self.diagram,
selected: self.fixture.project().id,
navigation_focus: None,
zoom: self.fixture.zoom,
pan: self.fixture.pan,
context: self.fixture.route_context().unwrap(),
selected_bend: None,
selected_attachment: None,
editable: false,
placing: false,
connection: None,
};
let canvas: Element<'_, crate::app::Message> = widget::canvas(OpticalCanvas {
program,
stamp: self.layout.expected,
drawn: self.drawn.clone(),
})
.width(Fill)
.height(Fill)
.into();
let body = if self.layout.variant == Variant::Baseline {
crate::views::optical_baseline(&self.fixture, canvas)
} else {
widget::sensor(canvas)
.key(generation)
.on_show(crate::app::Message::CanvasResized)
.on_resize(crate::app::Message::CanvasResized)
.into()
};
let body = inert::wrap(body, generation, self.layout.expected, self.drawn.clone());
if self.layout.variant == Variant::Baseline {
column![header, body].into()
} else {
column![
header,
body,
container(
text(&self.fixture.status)
.size(11)
.wrapping(widget::text::Wrapping::None)
)
.padding([6, 12])
.height(28)
.style(style::panel)
]
.into()
}
}
}
fn write_new(path: &Path, data: &[u8]) -> Result<()> {
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.and_then(|mut file| file.write_all(data))
.map_err(|e| format!("{}: {e}", path.display()))
}
fn write_json(path: &Path, value: &Value) -> Result<()> {
let mut bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
bytes.push(b'\n');
write_new(path, &bytes)
}
pub(crate) fn run(args: &[String]) -> iced::Result {
let prepare = || -> Result<Runner> {
if args.len() != 3 {
return Err(
"usage: arcadia-studio --focus-optical PROJECT.json DIAGRAM_UUID FRESH_OUTPUT_DIR"
.into(),
);
}
let input_path = fs::canonicalize(&args[0]).map_err(|e| e.to_string())?;
let input = fs::read(&input_path).map_err(|e| e.to_string())?;
let raw: Value = serde_json::from_slice(&input).map_err(|e| e.to_string())?;
let decoded =
oa_core::persistence::decode_with_report(&input).map_err(|e| e.to_string())?;
let diagram: Id = args[1]
.parse()
.map_err(|e| format!("Invalid diagram UUID: {e}"))?;
let frozen = Frozen::new(decoded.project, diagram)?;
let fixture = fixture(frozen.project.clone(), diagram)?;
frozen.check(&fixture, diagram)?;
let output = PathBuf::from(&args[2]);
fs::create_dir(&output).map_err(|e| format!("Fresh output directory required: {e}"))?;
write_new(&output.join("input.json"), &input)?;
write_json(
&output.join("admission.json"),
&json!({
"input_path": input_path, "input_bytes": input.len(), "raw_schema": raw.get("schema_version"),
"admitted_schema": frozen.project.schema_version, "migration": decoded.migration,
"diagnostics": oa_core::validation::validate(&frozen.project),
"diagram": diagram, "projection": frozen.project.diagrams[&diagram].projection,
"layer": frozen.project.diagrams[&diagram].layer,
"document_equals_admitted_complete_project": true,
"hash_identity": "Parent pins input/executable hashes externally; original input bytes retained.",
"scope": "Existing schema migration occurs once at admission; both variants use the same frozen admitted Project."
}),
)?;
write_json(
&output.join("project.json"),
&serde_json::to_value(&frozen.project).map_err(|e| e.to_string())?,
)?;
write_json(&output.join("scene.json"), &frozen.report)?;
write_new(&output.join("diagram.svg"), frozen.svg.as_bytes())?;
Ok(Runner {
fixture,
frozen,
diagram,
input_path,
input,
output,
layout: LayoutState::default(),
drawn: Rc::new(RefCell::new(None)),
header: None,
scale_factor: None,
capture: 0,
error: None,
})
};
let runner = match prepare() {
Ok(runner) => runner,
Err(error) => {
eprintln!("Focus optical admission failed: {error}");
std::process::exit(2);
}
};
let boot = RefCell::new(Some(runner));
iced::application(
move || {
(
boot.borrow_mut().take().expect("Optical runner boots once"),
iced::window::oldest().and_then(|id| {
iced::window::size(id).map(move |size| Message::Opened(id, size))
}),
)
},
Runner::update,
Runner::view,
)
.title("ARCADIA · private read-only optical comparison")
.theme(|_: &Runner| style::theme())
.subscription(|_: &Runner| {
iced::event::listen_with(|event, _, id| match event {
iced::Event::Window(iced::window::Event::Opened { size, .. }) => {
Some(Message::Opened(id, size))
}
iced::Event::Window(iced::window::Event::Resized(size)) => {
Some(Message::WindowSize(size))
}
iced::Event::Window(iced::window::Event::Rescaled(scale)) => {
Some(Message::Scale(scale))
}
iced::Event::Window(iced::window::Event::CloseRequested) => Some(Message::Close(id)),
_ => None,
})
})
.window(iced::window::Settings {
size: Size::new(1440., 900.),
min_size: Some(Size::new(1100., 700.)),
exit_on_close_request: false,
..Default::default()
})
.run()
}
@@ -0,0 +1,167 @@
//! Pre-dispatch boundary: frozen chrome never receives interactive events/operations.
use super::{DrawStamp, Mailbox, Message};
use crate::app::Message as AppMessage;
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, renderer,
widget::{Tree, tree},
};
use iced::{Element, Event, Length, Rectangle, Size, Theme, mouse};
pub(super) fn wrap<'a>(
content: Element<'a, AppMessage>,
generation: u64,
expected: Option<DrawStamp>,
drawn: Mailbox,
) -> Element<'a, Message> {
Element::new(Inert {
content,
generation,
expected,
drawn,
})
}
pub(super) struct Inert<'a, R: renderer::Renderer> {
pub(super) content: Element<'a, AppMessage, Theme, R>,
pub(super) generation: u64,
pub(super) expected: Option<DrawStamp>,
pub(super) drawn: Mailbox,
}
#[derive(Default)]
struct State {
generation: u64,
acknowledged: Option<DrawStamp>,
}
impl<R: renderer::Renderer> Widget<Message, Theme, R> for Inert<'_, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
generation: self.generation,
acknowledged: None,
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
if state.generation != self.generation {
// This rig owns no editable widget state. Remount so an unchanged-size
// Baseline sensor still reports after explicit Fit/generation change.
*state = State {
generation: self.generation,
acknowledged: None,
};
tree.children = self.children();
} else {
tree.diff_children(std::slice::from_ref(&self.content));
}
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.content.as_widget().size_hint()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
// Default operate/overlay/mouse_interaction do not delegate into hidden controls.
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
_: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if matches!(
event,
Event::Window(iced::window::Event::Resized(_) | iced::window::Event::Rescaled(_))
) {
*self.drawn.borrow_mut() = None;
}
if !matches!(
event,
Event::Window(iced::window::Event::RedrawRequested(_))
) {
return;
}
let mut messages = Vec::new();
let mut passive = Shell::new(&mut messages);
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
mouse::Cursor::Unavailable,
renderer,
clipboard,
&mut passive,
viewport,
);
shell.request_redraw_at(passive.redraw_request());
if passive.is_layout_invalid() {
shell.invalidate_layout();
}
if passive.are_widgets_invalid() {
shell.invalidate_widgets();
}
// Do not dispatch any production command, including an unexpected passive one.
for message in messages {
match message {
AppMessage::CanvasResized(size) => {
if self.expected.is_some_and(|stamp| stamp.viewport != size) {
*self.drawn.borrow_mut() = None;
}
shell.publish(Message::Measured(self.generation, size));
}
other => shell.publish(Message::UnexpectedPassive(format!(
"Frozen chrome emitted unexpected passive message {:?}",
std::mem::discriminant(&other)
))),
}
}
let state = tree.state.downcast_mut::<State>();
if let Some(stamp) = *self.drawn.borrow()
&& Some(stamp) == self.expected
&& state.acknowledged != Some(stamp)
{
state.acknowledged = Some(stamp);
shell.publish(Message::Drawn(stamp));
}
// draw runs after this update. One further redraw lets us acknowledge its
// exact receipt. Stop requesting once that receipt was delivered.
if self.expected.is_some() && state.acknowledged != self.expected {
shell.request_redraw();
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
_: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
mouse::Cursor::Unavailable,
viewport,
);
}
}
@@ -0,0 +1,612 @@
use super::*;
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, renderer,
widget::{Operation, Tree},
};
use iced::{Event, Length, Point};
use std::cell::Cell;
fn sample() -> Frozen {
let project = Project::demo();
let id = project
.diagrams
.values()
.find(|d| d.projection.is_architecture())
.unwrap()
.id;
Frozen::new(project, id).unwrap()
}
#[test]
fn fit_uses_both_axes_offsets_margin_clamp_and_empty_scene() {
let bounds = routing::RoutingBounds {
position: oa_core::Position { x: 54., y: 18. },
size: oa_core::Size {
width: 1506.,
height: 814.,
},
};
let width = fit(bounds, false, Size::new(826., 534.));
assert_eq!(width.zoom, 778. / 1506.);
let height = fit(bounds, false, Size::new(1406., 534.));
assert_eq!(height.zoom, 486. / 814.);
assert!((width.pan.x + 54. * width.zoom - 24.).abs() < 0.0001);
assert!((height.pan.y + 18. * height.zoom - 24.).abs() < 0.0001);
assert_eq!(fit(bounds, false, Size::new(10000., 10000.)).zoom, 1.5);
assert_eq!(
fit(bounds, true, Size::new(1., 1.)),
Camera {
zoom: 1.,
pan: Vector::new(30., 30.)
}
);
// Large nonzero origin makes algebraically simplified size differ in f32.
let offset = routing::RoutingBounds {
position: oa_core::Position {
x: 1_000_000.,
y: 500_000.,
},
size: oa_core::Size {
width: 100.03,
height: 80.02,
},
};
let camera = fit(offset, false, Size::new(100., 200.));
assert_eq!(
camera.zoom,
52. / ((offset.position.x + offset.size.width) - offset.position.x)
);
}
#[test]
fn measurements_and_draw_acknowledgments_reject_stale_invalid_repeat_and_wrong_camera() {
let frozen = sample();
let mut layout = LayoutState::default();
for size in [
Size::ZERO,
Size::new(f32::NAN, 10.),
Size::new(10., f32::INFINITY),
Size::new(-1., 20.),
] {
assert!(!layout.measured(0, size, &frozen));
}
assert!(layout.measured(0, Size::new(800., 500.), &frozen));
assert!(
!layout.ready(),
"Sensor receipt is not a draw acknowledgment"
);
let first = layout.expected.unwrap();
assert!(!layout.measured(0, first.viewport, &frozen));
assert!(!layout.drawn(DrawStamp {
camera: Camera {
zoom: first.camera.zoom * 1.1,
..first.camera
},
..first
}));
assert!(layout.drawn(first));
assert!(layout.ready());
assert!(!layout.drawn(first));
assert!(layout.measured(0, Size::new(900., 600.), &frozen));
assert_eq!(
layout.generation, 1,
"Same-variant real resize invalidates old authority"
);
assert!(!layout.drawn(first));
assert!(!layout.measured(0, Size::new(800., 500.), &frozen));
assert!(!layout.ready());
layout.variant = Variant::Focus;
layout.invalidate();
assert!(!layout.drawn(first));
assert!(!layout.measured(1, Size::new(1400., 800.), &frozen));
assert!(layout.measured(2, Size::new(1400., 800.), &frozen));
assert!(layout.drawn(layout.expected.unwrap()));
}
#[test]
fn admission_rejects_missing_state_and_scenario_views_without_project_mutation() {
let original = sample();
assert!(Frozen::new(original.project.clone(), Id::new_v4()).is_err());
for projection in [
oa_core::ProjectionKind::StateMachine {
machine: Id::new_v4(),
},
oa_core::ProjectionKind::Scenario {
scenario: Id::new_v4(),
},
] {
let mut candidate = original.project.clone();
candidate
.diagrams
.get_mut(&original.diagram)
.unwrap()
.projection = projection;
assert!(Frozen::new(candidate, original.diagram).is_err());
}
let app = fixture(original.project.clone(), original.diagram).unwrap();
original.check(&app, original.diagram).unwrap();
assert!(!app.document.dirty() && !app.document.can_undo() && !app.document.can_redo());
}
#[test]
fn fixed_schema_seven_and_migrated_schema_three_fixtures_admit_coherent_oa_sa_chrome() {
let cases: &[(&[u8], &str)] = &[
(
include_bytes!(
"../../../../harness/experiments/focus-optical-20261001/eight-card.json"
),
"00000000-0038-4000-8000-000000000001",
),
(
include_bytes!("../../../../examples/routing-workshop.json"),
"945a7c88-a352-5028-b808-73d852e5abe6",
),
(
include_bytes!("../../../../examples/routing-workshop.json"),
"cb21862d-79e1-5a9d-bdf0-ee344c2fb219",
),
(
include_bytes!("../../../../examples/coastal-rescue.json"),
"487c064d-df33-45f2-b4b7-6dd68d7b601f",
),
];
for (bytes, diagram) in cases {
let decoded = oa_core::persistence::decode_with_report(bytes).unwrap();
let raw: Value = serde_json::from_slice(bytes).unwrap();
assert_eq!(decoded.migration.is_some(), raw["schema_version"] == 3);
let frozen = Frozen::new(decoded.project, diagram.parse().unwrap()).unwrap();
let app = fixture(frozen.project.clone(), frozen.diagram).unwrap();
frozen.check(&app, frozen.diagram).unwrap();
assert_eq!(
app.create_kind,
if app.layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}
);
assert_eq!(app.draft.as_ref().unwrap().original, app.project().id);
assert_eq!(
app.draft.as_ref().unwrap().kind,
crate::forms::RecordKind::Project
);
assert!(
app.architecture.is_none()
&& app.connection.is_none()
&& app.diagram_navigation_focus.is_none()
);
let expected_index = crate::explorer::Index::new(app.project(), app.layer);
for group in crate::explorer::Group::ALL {
assert_eq!(
app.explorer_index.rows(group, "", &app.explorer_collapsed),
expected_index.rows(group, "", &app.explorer_collapsed)
);
}
}
}
#[test]
fn captures_require_a_matching_draw_and_preserve_complete_model_scene_svg() {
let frozen = sample();
let dir = tempfile::tempdir().unwrap();
let input = serde_json::to_vec(&frozen.project).unwrap();
let input_path = dir.path().join("fixture.json");
fs::write(&input_path, &input).unwrap();
let mut runner = Runner {
fixture: fixture(frozen.project.clone(), frozen.diagram).unwrap(),
diagram: frozen.diagram,
frozen,
input_path,
input,
output: dir.path().to_owned(),
layout: LayoutState::default(),
drawn: Rc::new(RefCell::new(None)),
header: None,
scale_factor: Some(1.),
capture: 0,
error: None,
};
let original_bundle = runner.fixture.document.bundle().clone();
assert!(runner.capture().is_err());
for (index, variant) in [Variant::Baseline, Variant::Focus, Variant::Baseline]
.into_iter()
.enumerate()
{
runner.layout.variant = variant;
runner.invalidate();
let generation = runner.layout.generation;
let size = if variant == Variant::Focus {
Size::new(1400., 800.)
} else {
Size::new(826., 534.)
};
let _ = runner.update(Message::Measured(generation, size));
runner.header = Some((generation, Size::new(1440., 67.4)));
assert!(runner.capture().is_err());
let stamp = runner.layout.expected.unwrap();
*runner.drawn.borrow_mut() = Some(stamp);
let _ = runner.update(Message::Drawn(stamp));
runner.capture().unwrap();
let captured = dir.path().join(format!("capture-{:03}", index + 1));
assert_eq!(
fs::read(captured.join("diagram.svg")).unwrap(),
runner.frozen.svg.as_bytes()
);
let scene: Value =
serde_json::from_slice(&fs::read(captured.join("scene.json")).unwrap()).unwrap();
assert_eq!(scene, runner.frozen.report);
let project: Project =
serde_json::from_slice(&fs::read(captured.join("project.json")).unwrap()).unwrap();
assert_eq!(project, runner.frozen.project);
assert_eq!(runner.fixture.document.bundle(), &original_bundle);
assert!(!runner.fixture.document.can_undo() && !runner.fixture.document.can_redo());
}
let mut altered = runner.frozen.project.clone();
altered.name.push_str(" changed");
let altered_app = fixture(altered, runner.diagram).unwrap();
assert!(runner.frozen.check(&altered_app, runner.diagram).is_err());
fs::write(&runner.input_path, b"changed").unwrap();
assert!(runner.capture().is_err());
}
#[test]
fn private_flag_is_exact_and_other_launches_remain_opted_out() {
for arguments in [
vec!["app"],
vec!["app", "--write-example", "out"],
vec!["app", "--focus"],
vec!["app", "--focus-optical-extra"],
] {
assert!(!requested(
&arguments.into_iter().map(String::from).collect::<Vec<_>>()
));
}
assert!(requested(&["app".into(), "--focus-optical".into()]));
}
#[test]
fn terminal_fault_rejects_queued_draw_capture_and_close_never_overwrites_or_traps() {
let frozen = sample();
let dir = tempfile::tempdir().unwrap();
let input = serde_json::to_vec(&frozen.project).unwrap();
let input_path = dir.path().join("fixture.json");
fs::write(&input_path, &input).unwrap();
let mut runner = Runner {
fixture: fixture(frozen.project.clone(), frozen.diagram).unwrap(),
diagram: frozen.diagram,
frozen,
input_path,
input,
output: dir.path().to_owned(),
layout: LayoutState::default(),
drawn: Rc::new(RefCell::new(None)),
header: None,
scale_factor: Some(1.),
capture: 0,
error: None,
};
let _ = runner.update(Message::Measured(0, Size::new(800., 500.)));
let stamp = runner.layout.expected.unwrap();
runner.header = Some((0, Size::new(1440., 67.4)));
*runner.drawn.borrow_mut() = Some(stamp);
let _ = runner.update(Message::Drawn(stamp));
assert!(runner.layout.ready());
runner.fail("Injected harness fault".into());
let _ = runner.update(Message::Drawn(stamp));
let _ = runner.update(Message::Capture);
assert!(!runner.layout.ready());
assert!(runner.capture().is_err());
assert_eq!(runner.capture, 0);
assert!(!dir.path().join("capture-001").exists());
fs::write(&runner.input_path, b"changed after admission").unwrap();
let close = runner.update(Message::Close(iced::window::Id::unique()));
assert!(
iced_runtime::task::into_stream(close).is_some(),
"Failure still issues the exit task"
);
let receipt = dir.path().join("finished.json");
let original = fs::read(&receipt).unwrap();
let recorded: Value = serde_json::from_slice(&original).unwrap();
assert_eq!(recorded["status"], "FAIL");
assert_eq!(
recorded["complete_project_scene_svg_and_input_unchanged"],
false
);
let close_again = runner.update(Message::Close(iced::window::Id::unique()));
assert!(
iced_runtime::task::into_stream(close_again).is_some(),
"Existing receipt cannot trap Close"
);
assert_eq!(
fs::read(receipt).unwrap(),
original,
"Never overwrite the first close receipt"
);
}
#[derive(Default)]
struct SpyCounts {
updates: Cell<usize>,
operations: Cell<usize>,
overlays: Cell<usize>,
draws: Cell<usize>,
}
struct Spy {
counts: Rc<SpyCounts>,
unexpected: bool,
}
impl Widget<crate::app::Message, Theme, ()> for Spy {
fn size(&self) -> Size<Length> {
Size::new(Fill, Fill)
}
fn layout(&mut self, _: &mut Tree, _: &(), _: &layout::Limits) -> layout::Node {
layout::Node::new(Size::new(500., 400.))
}
fn update(
&mut self,
_: &mut Tree,
event: &Event,
_: Layout<'_>,
cursor: mouse::Cursor,
_: &(),
_: &mut dyn Clipboard,
shell: &mut Shell<'_, crate::app::Message>,
_: &Rectangle,
) {
self.counts.updates.set(self.counts.updates.get() + 1);
assert!(matches!(
event,
Event::Window(iced::window::Event::RedrawRequested(_))
));
assert_eq!(cursor, mouse::Cursor::Unavailable);
shell.publish(if self.unexpected {
crate::app::Message::Fit
} else {
crate::app::Message::CanvasResized(Size::new(500., 400.))
});
}
fn operate(&mut self, _: &mut Tree, _: Layout<'_>, _: &(), _: &mut dyn Operation) {
self.counts.operations.set(self.counts.operations.get() + 1);
}
fn overlay<'a>(
&'a mut self,
_: &'a mut Tree,
_: Layout<'a>,
_: &(),
_: &Rectangle,
_: Vector,
) -> Option<iced::advanced::overlay::Element<'a, crate::app::Message, Theme, ()>> {
self.counts.overlays.set(self.counts.overlays.get() + 1);
None
}
fn draw(
&self,
_: &Tree,
_: &mut (),
_: &Theme,
_: &renderer::Style,
_: Layout<'_>,
cursor: mouse::Cursor,
_: &Rectangle,
) {
assert_eq!(cursor, mouse::Cursor::Unavailable);
self.counts.draws.set(self.counts.draws.get() + 1);
}
}
struct NoClipboard;
impl Clipboard for NoClipboard {
fn read(&self, _: iced::advanced::clipboard::Kind) -> Option<String> {
panic!("Unexpected clipboard read")
}
fn write(&mut self, _: iced::advanced::clipboard::Kind, _: String) {
panic!("Unexpected clipboard write")
}
}
struct Traverse;
impl Operation for Traverse {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
}
#[test]
fn inert_widget_blocks_all_input_operations_overlays_and_allows_only_redraw_measurement() {
let counts = Rc::new(SpyCounts::default());
let mut wrapper = inert::Inert {
content: Element::new(Spy {
counts: counts.clone(),
unexpected: false,
}),
generation: 9,
expected: None,
drawn: Rc::new(RefCell::new(None)),
};
let mut tree = Tree::new(&wrapper as &dyn Widget<Message, Theme, ()>);
let node = wrapper.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(500., 400.)),
);
let layout = Layout::new(&node);
let bounds = Rectangle::with_size(Size::new(500., 400.));
let cursor = mouse::Cursor::Available(Point::new(40., 40.));
let key = crate::behavior::canvas_workspace::tests::key;
let mut events = vec![
Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(50., 50.),
}),
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
}),
key(iced::keyboard::key::Named::Delete),
key(iced::keyboard::key::Named::Insert),
key(iced::keyboard::key::Named::Tab),
Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
iced::keyboard::Modifiers::SHIFT,
)),
Event::InputMethod(iced::advanced::input_method::Event::Preedit(
"字".into(),
None,
)),
Event::InputMethod(iced::advanced::input_method::Event::Commit("字".into())),
Event::Touch(iced::touch::Event::FingerPressed {
id: iced::touch::Finger(1),
position: Point::ORIGIN,
}),
Event::Window(iced::window::Event::Resized(Size::new(600., 500.))),
Event::Window(iced::window::Event::Rescaled(1.25)),
Event::Window(iced::window::Event::Unfocused),
Event::Window(iced::window::Event::FileDropped(PathBuf::from(
"ignored.json",
))),
];
for button in [
mouse::Button::Left,
mouse::Button::Middle,
mouse::Button::Right,
] {
events.push(Event::Mouse(mouse::Event::ButtonPressed(button)));
events.push(Event::Mouse(mouse::Event::ButtonReleased(button)));
}
let mut messages = vec![];
for event in &events {
wrapper.update(
&mut tree,
event,
layout,
cursor,
&(),
&mut NoClipboard,
&mut Shell::new(&mut messages),
&bounds,
);
}
assert_eq!(counts.updates.get(), 0);
assert!(messages.is_empty());
wrapper.operate(&mut tree, layout, &(), &mut Traverse);
assert!(
wrapper
.overlay(&mut tree, layout, &(), &bounds, Vector::ZERO)
.is_none()
);
assert_eq!(
wrapper.mouse_interaction(&tree, layout, cursor, &bounds, &()),
mouse::Interaction::None
);
assert_eq!(counts.operations.get() + counts.overlays.get(), 0);
wrapper.draw(
&tree,
&mut (),
&Theme::Light,
&renderer::Style::default(),
layout,
cursor,
&bounds,
);
assert_eq!(counts.draws.get(), 1);
wrapper.update(
&mut tree,
&Event::Window(iced::window::Event::RedrawRequested(
iced::time::Instant::now(),
)),
layout,
cursor,
&(),
&mut NoClipboard,
&mut Shell::new(&mut messages),
&bounds,
);
assert!(
matches!(messages.as_slice(), [Message::Measured(9, size)] if *size == Size::new(500., 400.))
);
assert_eq!(counts.updates.get(), 1);
}
#[test]
fn unexpected_passive_message_is_a_fault_and_draw_only_adapter_never_emits_actions() {
let counts = Rc::new(SpyCounts::default());
let mut wrapper = inert::Inert {
content: Element::new(Spy {
counts,
unexpected: true,
}),
generation: 0,
expected: None,
drawn: Rc::new(RefCell::new(None)),
};
let mut tree = Tree::new(&wrapper as &dyn Widget<Message, Theme, ()>);
let node = wrapper.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(500., 400.)),
);
let mut messages = vec![];
wrapper.update(
&mut tree,
&Event::Window(iced::window::Event::RedrawRequested(
iced::time::Instant::now(),
)),
Layout::new(&node),
mouse::Cursor::Unavailable,
&(),
&mut NoClipboard,
&mut Shell::new(&mut messages),
&Rectangle::with_size(Size::new(500., 400.)),
);
assert!(matches!(
messages.as_slice(),
[Message::UnexpectedPassive(_)]
));
let frozen = sample();
let app = fixture(frozen.project.clone(), frozen.diagram).unwrap();
let program = OpticalCanvas {
program: DiagramCanvas {
selection: None,
allocation: None,
project: app.project(),
view_index: Some(app.document.view_index()),
diagram: frozen.diagram,
selected: app.project().id,
navigation_focus: None,
zoom: 1.,
pan: Vector::ZERO,
context: app.route_context().unwrap(),
selected_bend: None,
selected_attachment: None,
editable: false,
placing: false,
connection: None,
},
stamp: None,
drawn: Rc::new(RefCell::new(None)),
};
let mut state = crate::diagram::State::default();
use canvas::Program;
for event in [
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(300., 200.),
}),
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
] {
assert!(
program
.update(
&mut state,
&event,
Rectangle::with_size(Size::new(500., 400.)),
mouse::Cursor::Available(Point::new(200., 100.))
)
.is_none()
);
}
assert_eq!(
program.mouse_interaction(
&state,
Rectangle::with_size(Size::new(500., 400.)),
mouse::Cursor::Available(Point::new(200., 100.))
),
mouse::Interaction::None
);
frozen.check(&app, frozen.diagram).unwrap();
}
+4
View File
@@ -15,6 +15,7 @@ mod diagram_usage;
mod document;
mod explorer;
mod explorer_view;
mod focus_optical;
mod forms;
mod guide;
mod highlight;
@@ -44,6 +45,9 @@ mod workflow;
fn main() -> iced::Result {
// Small headless entry points support reproducible examples and CI validation.
let args: Vec<_> = std::env::args().collect();
if focus_optical::requested(&args) {
return focus_optical::run(&args[2..]);
}
if args.get(1).is_some_and(|s| s == "--write-example") {
let path = args
.get(2)
+36 -7
View File
@@ -65,8 +65,18 @@ pub fn heading<'a>(title: &str, subtitle: &str) -> View<'a, Message> {
}
pub fn view(app: &App) -> View<'_, Message> {
workspace(app, None)
}
/// Private optical rig: preserve actual Baseline chrome below the shared header.
pub(crate) fn optical_baseline<'a>(app: &'a App, canvas: View<'a, Message>) -> View<'a, Message> {
workspace(app, Some(canvas))
}
/// Same logo, project typography, padding and intrinsic height in both runners.
pub(crate) fn header<'a, M: 'a>(app: &App, actions: View<'a, M>) -> View<'a, M> {
let p = app.project();
let header = container(
container(
row![
row![
text("◈").size(28).color(style::TEAL),
@@ -81,6 +91,22 @@ pub fn view(app: &App) -> View<'_, Message> {
column![text(short(&p.name, 46)).size(17), label(app.hosted_label())]
.spacing(3)
.width(Fill),
actions
]
.spacing(8)
.align_y(iced::Center),
)
.padding([14, 18])
.style(style::panel)
.into()
}
fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<'a, Message> {
let p = app.project();
let optical = optical_canvas.is_some();
let header = header(
app,
row![
action("New", Message::Request(Pending::New)),
crate::hosted::view::header_action(),
action("Open", Message::Request(Pending::Open)),
@@ -90,10 +116,9 @@ pub fn view(app: &App) -> View<'_, Message> {
.on_press(Message::Save(false))
]
.spacing(8)
.align_y(iced::Center),
)
.padding([14, 18])
.style(style::panel);
.align_y(iced::Center)
.into(),
);
let mut nav = column![
label("ENGINEERING PERSPECTIVE"),
row![
@@ -240,7 +265,7 @@ pub fn view(app: &App) -> View<'_, Message> {
&app.recovery,
)
.map(Message::Home),
Page::Diagram => diagram(app),
Page::Diagram => diagram(app, optical_canvas),
Page::Catalog => crate::explorer_view::catalog(app),
Page::Data => data_catalog(app),
Page::Behavior => crate::behavior::view(app),
@@ -295,6 +320,9 @@ pub fn view(app: &App) -> View<'_, Message> {
)
.padding([4, 16])
.style(style::panel);
if optical {
return column![task_nav, body, footer].into();
}
let root = crate::allocation_input::root(app, column![header, task_nav, body, footer]);
let root = crate::guide::protect_workspace(app.context_help, root);
if app.context_help {
@@ -439,7 +467,7 @@ fn modal_width<'a>(
]
.into()
}
fn diagram(app: &App) -> View<'_, Message> {
fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<'a, Message> {
if app
.active_diagram
.and_then(|id| app.project().diagrams.get(&id))
@@ -569,6 +597,7 @@ fn diagram(app: &App) -> View<'_, Message> {
if let Some(editor) = crate::app::architecture_authoring::inline_editor(app) {
scene = scene.push(editor);
}
let scene: View<'_, Message> = optical_canvas.unwrap_or_else(|| scene.into());
let reveal = app.architecture_reveal_stamp(id);
row![
widget::sensor(