WIP checkpoint shared behavior canvas before user reboot
This commit is contained in:
@@ -16,6 +16,7 @@ use oa_core::*;
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(test)]
|
||||
mod contextual_source_tests;
|
||||
pub(crate) mod canvas_workspace;
|
||||
pub(crate) mod creation;
|
||||
pub(crate) mod scenario;
|
||||
// Projection acceptance precedes the coordinated scenario renderer integration.
|
||||
@@ -1233,6 +1234,7 @@ struct Scene {
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
scroll_offset: iced::Vector,
|
||||
camera_pan: iced::Vector,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
@@ -1287,6 +1289,7 @@ impl Scene {
|
||||
let mut scene = Self {
|
||||
scale: 1.,
|
||||
scroll_offset: iced::Vector::ZERO,
|
||||
camera_pan: iced::Vector::ZERO,
|
||||
boxes: vec![],
|
||||
lines: vec![],
|
||||
size: Size::new(700., 200.),
|
||||
@@ -1951,7 +1954,7 @@ impl canvas::Program<Message> for Scene {
|
||||
};
|
||||
let world = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
.map(|p| Point::new((p.x - self.camera_pan.x) / self.scale, (p.y - self.camera_pan.y) / self.scale));
|
||||
match event {
|
||||
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if !visible {
|
||||
@@ -1988,10 +1991,11 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if self.reorder_handle().is_some_and(|r| r.contains(point)) {
|
||||
if let Some(occurrence) = self.hit(point).filter(|id| self.rows.contains_key(id))
|
||||
.or_else(|| self.reorder_handle().filter(|r| r.contains(point)).map(|_| self.selected)) {
|
||||
state.press = Some(SequencePress::Reorder {
|
||||
occurrence: self.selected,
|
||||
parent: self.rows[&self.selected].0,
|
||||
occurrence,
|
||||
parent: self.rows[&occurrence].0,
|
||||
start: cursor.position()? - self.scroll_offset,
|
||||
moved: false,
|
||||
});
|
||||
@@ -2066,6 +2070,9 @@ impl canvas::Program<Message> for Scene {
|
||||
);
|
||||
}
|
||||
}
|
||||
SequencePress::Reorder { occurrence, moved: false, .. } => {
|
||||
return Some(Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(context, occurrence))).and_capture());
|
||||
}
|
||||
SequencePress::Reorder {
|
||||
occurrence,
|
||||
parent,
|
||||
@@ -2103,6 +2110,7 @@ impl canvas::Program<Message> for Scene {
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
f.translate(self.camera_pan);
|
||||
f.scale(self.scale);
|
||||
for b in &self.boxes {
|
||||
let outline = matches!(
|
||||
@@ -2216,7 +2224,7 @@ impl canvas::Program<Message> for Scene {
|
||||
let active = editable && state.context == Some(context) && state.tool == tool;
|
||||
let point = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
.map(|p| Point::new((p.x - self.camera_pan.x) / self.scale, (p.y - self.camera_pan.y) / self.scale));
|
||||
let source = active.then_some(state.source).flatten();
|
||||
let reorder = active.then_some(state.press).flatten().and_then(|press| {
|
||||
if let SequencePress::Reorder { parent, .. } = press {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Shared presentation and input contract; contains no model commands.
|
||||
use iced::{Element, Length::Fill, Point, Theme, Vector};
|
||||
use iced::widget::{button, column, container, opaque, row, scrollable, stack, text, tooltip};
|
||||
use oa_core::Id;
|
||||
mod viewport;
|
||||
pub use viewport::Viewport;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Scope {
|
||||
pub document: Id,
|
||||
pub owner: Id,
|
||||
pub revision: u64,
|
||||
}
|
||||
impl Scope {
|
||||
pub fn widget_id(self) -> iced::advanced::widget::Id {
|
||||
iced::advanced::widget::Id::new(format!("behavior-canvas:{}:{}", self.document, self.owner))
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Event {
|
||||
Pan(Vector),
|
||||
Zoom { factor: f32, at: Point, base: f32 },
|
||||
Focus,
|
||||
Traverse(bool),
|
||||
}
|
||||
/// Fold relative input in event order. The base scale is used only on leaving Fit.
|
||||
pub fn change(pan: &mut Vector, zoom: &mut Option<f32>, fit: &mut bool, event: Event) -> bool {
|
||||
match event {
|
||||
Event::Pan(delta) if delta.x.is_finite() && delta.y.is_finite() => {
|
||||
let next = *pan + delta;
|
||||
if next.x.is_finite() && next.y.is_finite() && next != *pan {
|
||||
*pan = next;
|
||||
true
|
||||
} else { false }
|
||||
}
|
||||
Event::Zoom { factor, at, base }
|
||||
if factor.is_finite() && factor > 0. && base.is_finite() && base > 0.
|
||||
&& at.x.is_finite() && at.y.is_finite() =>
|
||||
{
|
||||
let old = if *fit { base } else { zoom.unwrap_or(base) };
|
||||
let next = (old * factor).clamp(0.05, 3.);
|
||||
if old == next { return false; }
|
||||
let anchor = Vector::new(at.x, at.y);
|
||||
*pan = anchor - (anchor - *pan) * (next / old);
|
||||
*zoom = Some(next);
|
||||
*fit = false;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constant-width labelled palette. Arming a tool never reallocates the canvas.
|
||||
pub fn palette<'a, Message: Clone + 'a>(
|
||||
entries: impl IntoIterator<Item = (&'static str, &'static str, bool, Option<Message>)>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut tools = column![].spacing(3);
|
||||
for (glyph, label, selected, message) in entries {
|
||||
tools = tools.push(tooltip(
|
||||
button(text(glyph).size(12)).width(34).height(30).padding(3)
|
||||
.style(if selected { button::primary } else { crate::style::nav })
|
||||
.on_press_maybe(message),
|
||||
container(text(label).size(12)).padding(7).style(crate::style::card),
|
||||
tooltip::Position::Right,
|
||||
));
|
||||
}
|
||||
container(scrollable(tools)).width(40).height(Fill).padding(3)
|
||||
.style(crate::style::panel).into()
|
||||
}
|
||||
|
||||
/// Header and palette are permanent. Details/editors overlay the same viewport.
|
||||
pub fn host<'a, Message: 'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
header: Element<'a, Message, Theme, R>,
|
||||
palette: Element<'a, Message, Theme, R>,
|
||||
canvas: Element<'a, Message, Theme, R>,
|
||||
editor: Option<Element<'a, Message, Theme, R>>,
|
||||
) -> Element<'a, Message, Theme, R> {
|
||||
let base = row![canvas, palette].width(Fill).height(Fill);
|
||||
let mut body = stack![base];
|
||||
if let Some(editor) = editor {
|
||||
body = body.push(
|
||||
container(opaque(container(editor).width(320).height(Fill).style(crate::style::card)))
|
||||
.align_right(Fill).height(Fill).padding(8),
|
||||
);
|
||||
}
|
||||
column![header, body].spacing(6).padding(8).width(Fill).height(Fill).into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,149 @@
|
||||
use super::{Event as CameraEvent, Scope};
|
||||
use iced::{Border, Element, Event, Length, Point, Rectangle, Size, Theme, Vector};
|
||||
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer,
|
||||
widget::{Operation, Tree, operation, tree}};
|
||||
use iced::{keyboard, mouse};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Input {
|
||||
scope: Option<Scope>,
|
||||
focused: bool,
|
||||
pointer: Option<Point>,
|
||||
pan: Option<(mouse::Button, Point)>,
|
||||
space: bool,
|
||||
}
|
||||
impl operation::Focusable for Input {
|
||||
fn is_focused(&self) -> bool { self.focused }
|
||||
fn focus(&mut self) { self.focused = true; }
|
||||
fn unfocus(&mut self) { self.focused = false; self.pan = None; self.space = false; }
|
||||
}
|
||||
pub struct Viewport<'a, Message, R = iced::Renderer>
|
||||
where R: renderer::Renderer {
|
||||
content: Element<'a, Message, Theme, R>,
|
||||
scope: Scope,
|
||||
scale: f32,
|
||||
keyboard: bool,
|
||||
publish: Box<dyn Fn(CameraEvent) -> Message + 'a>,
|
||||
}
|
||||
impl<'a, Message, R: renderer::Renderer> Viewport<'a, Message, R> {
|
||||
pub fn new(content: impl Into<Element<'a, Message, Theme, R>>, scope: Scope,
|
||||
scale: f32, keyboard: bool, publish: impl Fn(CameraEvent) -> Message + 'a) -> Self {
|
||||
Self { content: content.into(), scope, scale, keyboard, publish: Box::new(publish) }
|
||||
}
|
||||
}
|
||||
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_, Message, R> {
|
||||
fn tag(&self) -> tree::Tag { tree::Tag::of::<Input>() }
|
||||
fn state(&self) -> tree::State { tree::State::new(Input { scope: Some(self.scope), ..Default::default() }) }
|
||||
fn children(&self) -> Vec<Tree> { vec![Tree::new(self.content.as_widget())] }
|
||||
fn diff(&self, tree: &mut Tree) {
|
||||
let state = tree.state.downcast_mut::<Input>();
|
||||
if state.scope != Some(self.scope) { *state = Input { scope: Some(self.scope), ..Default::default() }; }
|
||||
tree.diff_children(std::slice::from_ref(&self.content));
|
||||
}
|
||||
fn size(&self) -> Size<Length> { Size::new(Length::Fill, Length::Fill) }
|
||||
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)
|
||||
}
|
||||
fn operate(&mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &R, operation: &mut dyn Operation) {
|
||||
operation.focusable(Some(&self.scope.widget_id()), layout.bounds(), tree.state.downcast_mut::<Input>());
|
||||
self.content.as_widget_mut().operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
fn update(&mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor,
|
||||
renderer: &R, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle) {
|
||||
let state = tree.state.downcast_mut::<Input>();
|
||||
if let Event::Mouse(mouse::Event::CursorMoved { position }) = event { state.pointer = Some(*position); }
|
||||
let pointer = match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_)) =>
|
||||
state.pointer.or_else(|| cursor.position()),
|
||||
_ => cursor.position().or(state.pointer),
|
||||
};
|
||||
let visible = layout.bounds().intersection(viewport);
|
||||
let inside = pointer.is_some_and(|p| visible.is_some_and(|v| v.contains(p))) && cursor.position().is_some();
|
||||
let mut camera = None;
|
||||
let mut capture = false;
|
||||
match event {
|
||||
Event::Window(iced::window::Event::Unfocused | iced::window::Event::Resized(_)) => {
|
||||
state.pan = None; state.space = false; state.focused = false;
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorLeft) => { state.pan = None; state.pointer = None; }
|
||||
Event::Mouse(mouse::Event::ButtonPressed(button)) => {
|
||||
state.focused = inside;
|
||||
if inside && (*button == mouse::Button::Middle || (*button == mouse::Button::Left && state.space)) {
|
||||
state.pan = pointer.map(|p| (*button, p));
|
||||
capture = true;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
||||
if let Some((button, previous)) = state.pan {
|
||||
state.pan = Some((button, *position));
|
||||
camera = Some(CameraEvent::Pan(*position - previous));
|
||||
capture = true;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(button)) => {
|
||||
if let Some((held, previous)) = state.pan && held == *button {
|
||||
state.pan = None;
|
||||
camera = pointer.filter(|_| inside).map(|p| CameraEvent::Pan(p - previous));
|
||||
capture = true;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta }) if inside => {
|
||||
state.pan = None;
|
||||
let y = match delta { mouse::ScrollDelta::Lines { y, .. } => *y,
|
||||
mouse::ScrollDelta::Pixels { y, .. } => *y / 40. };
|
||||
if y != 0. {
|
||||
let at = pointer.unwrap() - Vector::new(layout.bounds().x, layout.bounds().y);
|
||||
camera = Some(CameraEvent::Zoom { factor: 1.1_f32.powf(y.clamp(-8., 8.)), at, base: self.scale });
|
||||
capture = true;
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) if state.focused && self.keyboard => {
|
||||
use keyboard::{Key, key::Named};
|
||||
match key.as_ref() {
|
||||
Key::Named(Named::Space) => { state.space = true; capture = true; }
|
||||
Key::Named(Named::Tab) => { camera = Some(CameraEvent::Traverse(modifiers.shift())); capture = true; }
|
||||
Key::Named(Named::Escape) => { state.pan = None; state.space = false; }
|
||||
Key::Named(key) if modifiers.is_empty() => {
|
||||
let delta = match key {
|
||||
Named::ArrowLeft => Some(Vector::new(60., 0.)),
|
||||
Named::ArrowRight => Some(Vector::new(-60., 0.)),
|
||||
Named::ArrowUp => Some(Vector::new(0., 60.)),
|
||||
Named::ArrowDown => Some(Vector::new(0., -60.)),
|
||||
_ => None,
|
||||
};
|
||||
camera = delta.map(CameraEvent::Pan); capture = camera.is_some();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyReleased { key, .. })
|
||||
if *key == keyboard::Key::Named(keyboard::key::Named::Space) => { state.space = false; }
|
||||
_ => {}
|
||||
}
|
||||
if let Some(event) = camera { shell.publish((self.publish)(event)); }
|
||||
if capture { shell.capture_event(); shell.request_redraw(); return; }
|
||||
self.content.as_widget_mut().update(&mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport);
|
||||
}
|
||||
fn draw(&self, tree: &Tree, renderer: &mut R, theme: &Theme, style: &renderer::Style,
|
||||
layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle) {
|
||||
renderer.with_layer(layout.bounds(), |renderer| {
|
||||
self.content.as_widget().draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport);
|
||||
});
|
||||
if tree.state.downcast_ref::<Input>().focused && self.keyboard {
|
||||
renderer.fill_quad(renderer::Quad { bounds: layout.bounds(),
|
||||
border: Border { color: crate::style::TEAL, width: 1., ..Default::default() },
|
||||
..Default::default() }, iced::Color::TRANSPARENT);
|
||||
}
|
||||
}
|
||||
fn mouse_interaction(&self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &R) -> mouse::Interaction {
|
||||
if tree.state.downcast_ref::<Input>().pan.is_some() { mouse::Interaction::Grabbing }
|
||||
else { self.content.as_widget().mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer) }
|
||||
}
|
||||
fn overlay<'a>(&'a mut self, tree: &'a mut Tree, layout: Layout<'a>, renderer: &R, viewport: &Rectangle, translation: Vector)
|
||||
-> Option<overlay::Element<'a, Message, Theme, R>> {
|
||||
self.content.as_widget_mut().overlay(&mut tree.children[0], layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
impl<'a, Message: 'a, R: renderer::Renderer + 'a> From<Viewport<'a, Message, R>> for Element<'a, Message, Theme, R> {
|
||||
fn from(viewport: Viewport<'a, Message, R>) -> Self { Element::new(viewport) }
|
||||
}
|
||||
@@ -101,6 +101,7 @@ pub enum CanvasEvent {
|
||||
Viewport(Context, Size),
|
||||
FormShown(Context, Id),
|
||||
Zoom(Context, Option<f32>),
|
||||
Camera(Context, super::canvas_workspace::Event),
|
||||
Focus(Context),
|
||||
Shelf(Context, Shelf),
|
||||
OpenUse(Context, Id),
|
||||
@@ -141,6 +142,7 @@ impl CanvasEvent {
|
||||
| Self::Viewport(context, _)
|
||||
| Self::FormShown(context, _)
|
||||
| Self::Zoom(context, _)
|
||||
| Self::Camera(context, _)
|
||||
| Self::Focus(context)
|
||||
| Self::Shelf(context, _)
|
||||
| Self::OpenUse(context, _)
|
||||
@@ -330,6 +332,10 @@ impl State {
|
||||
pub(crate) fn viewport(&mut self, size: Size) -> bool {
|
||||
self.presentation.viewport(size)
|
||||
}
|
||||
pub(crate) fn camera(&mut self, event: super::canvas_workspace::Event) -> bool {
|
||||
let p = &mut self.presentation;
|
||||
super::canvas_workspace::change(&mut p.pan, &mut p.zoom, &mut p.fit, event)
|
||||
}
|
||||
pub(crate) fn zoom(&mut self, scale: Option<f32>) -> bool {
|
||||
self.presentation.zoom(scale)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(super) struct Presentation {
|
||||
pub(super) viewport: Option<Size>,
|
||||
pub(super) focused: bool,
|
||||
pub(super) shelf: Option<Shelf>,
|
||||
pub(super) pan: iced::Vector,
|
||||
pub(super) zoom: Option<f32>,
|
||||
pub(super) fit: bool,
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ pub struct State {
|
||||
return_to: Vec<(Id, Id, Option<oa_core::topology_context::ContextAddress>)>,
|
||||
draft: Option<Draft>,
|
||||
viewport: Option<Size>,
|
||||
pan: iced::Vector,
|
||||
zoom: Option<f32>,
|
||||
fit: bool,
|
||||
pub(crate) focused: bool,
|
||||
@@ -970,6 +971,7 @@ struct Graph<'a> {
|
||||
project: &'a Project,
|
||||
interaction: Option<(Context, CanvasTool, bool)>,
|
||||
scroll_offset: iced::Vector,
|
||||
camera_pan: iced::Vector,
|
||||
nodes: Vec<GraphNode>,
|
||||
edges: Vec<GraphEdge>,
|
||||
ordered_paths: Vec<OrderedPath>,
|
||||
@@ -1302,6 +1304,7 @@ impl<'a> Graph<'a> {
|
||||
project: p,
|
||||
interaction: None,
|
||||
scroll_offset: iced::Vector::ZERO,
|
||||
camera_pan: iced::Vector::ZERO,
|
||||
nodes,
|
||||
edges,
|
||||
ordered_paths,
|
||||
@@ -1401,6 +1404,7 @@ impl canvas::Program<Message> for Graph<'_> {
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
f.translate(self.camera_pan);
|
||||
f.scale(self.scale);
|
||||
for pass in PAINT_PASSES {
|
||||
match pass {
|
||||
@@ -1616,7 +1620,7 @@ impl canvas::Program<Message> for Graph<'_> {
|
||||
cursor: mouse::Cursor,
|
||||
) -> mouse::Interaction {
|
||||
if cursor.position_in(bounds).is_some_and(|p| {
|
||||
let p = Point::new(p.x / self.scale, p.y / self.scale);
|
||||
let p = Point::new((p.x - self.camera_pan.x) / self.scale, (p.y - self.camera_pan.y) / self.scale);
|
||||
self.nodes.iter().any(|n| n.rect.contains(p))
|
||||
|| self.ordered_paths.iter().any(|path| {
|
||||
path.points.windows(2).any(|pair| {
|
||||
|
||||
@@ -60,6 +60,7 @@ pub enum CanvasEvent {
|
||||
Open(Context, Id),
|
||||
Back(Context),
|
||||
Zoom(Context, Option<f32>),
|
||||
Camera(Context, super::super::canvas_workspace::Event),
|
||||
Focus(Context),
|
||||
Viewport(Context, Size),
|
||||
}
|
||||
@@ -77,6 +78,7 @@ impl CanvasEvent {
|
||||
| Self::Back(c)
|
||||
| Self::AppendExchange(c)
|
||||
| Self::Zoom(c, ..)
|
||||
| Self::Camera(c, ..)
|
||||
| Self::Focus(c)
|
||||
| Self::Viewport(c, ..) => *c,
|
||||
Self::Begin { context, .. }
|
||||
@@ -138,6 +140,7 @@ impl State {
|
||||
if self.identity != Some((document, owner)) {
|
||||
self.identity = Some((document, owner));
|
||||
self.tool = CanvasTool::Select;
|
||||
self.pan = iced::Vector::ZERO;
|
||||
self.zoom = Some(1.);
|
||||
self.fit = false;
|
||||
self.viewport = None;
|
||||
@@ -162,6 +165,9 @@ impl State {
|
||||
// Choosing how to edit must retain the user's current view.
|
||||
self.tool = tool;
|
||||
}
|
||||
pub(crate) fn camera(&mut self, event: super::super::canvas_workspace::Event) -> bool {
|
||||
super::super::canvas_workspace::change(&mut self.pan, &mut self.zoom, &mut self.fit, event)
|
||||
}
|
||||
pub(crate) fn zoom(&mut self, value: Option<f32>) {
|
||||
self.fit = value.is_none();
|
||||
self.zoom = value.map(|v| v.clamp(0.5, 2.));
|
||||
@@ -577,7 +583,7 @@ impl Graph<'_> {
|
||||
};
|
||||
let world = cursor
|
||||
.position_in(bounds)
|
||||
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
|
||||
.map(|p| Point::new((p.x - self.camera_pan.x) / self.scale, (p.y - self.camera_pan.y) / self.scale));
|
||||
let publish = |event| Some(Action::publish(Message::TopologyCanvas(event)).and_capture());
|
||||
match event {
|
||||
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if visible => {
|
||||
@@ -738,7 +744,7 @@ impl Graph<'_> {
|
||||
.map(|p| p + self.scroll_offset)
|
||||
.or_else(|| cursor.position())
|
||||
.filter(|p| bounds.contains(*p))
|
||||
.map(|p| Point::new((p.x - bounds.x) / self.scale, (p.y - bounds.y) / self.scale));
|
||||
.map(|p| Point::new((p.x - bounds.x - self.camera_pan.x) / self.scale, (p.y - bounds.y - self.camera_pan.y) / self.scale));
|
||||
let live =
|
||||
state.context == Some(context) && state.tool == tool && state.bounds == Some(bounds);
|
||||
if live && let Some(source) = state.source {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Canvas phase 1 — interrupted WIP handoff
|
||||
|
||||
Codex-956atf /root/behavior_audit. Implementation stopped immediately on root's user-requested reboot checkpoint, 2026-09-11. **Frozen; do not resume until root's post-reboot follow-up.**
|
||||
|
||||
Workspace: C:/Users/User/Documents/Git/ARCADIA-Studio/.local/behavior-canvas-worktree-20260911
|
||||
Branch assigned by root: codex/behavior-canvas-20260911
|
||||
Verified starting commit: c0e66f94e3412b5c3495e96f7934ae94116505c9
|
||||
|
||||
This is intentionally unfinished and **not compile-coherent**. No Cargo/rustc/rustfmt/native/Git mutation was run by this worker. Original checkout was not edited. Root owns WIP commit/push and reboot-safe confirmation. Coordination: issue #2 comment 261; ratified design is release-unified-behavior-canvas-design-20260911.md. User clarification #2/250 explicitly permits semantic sequence constraints.
|
||||
|
||||
## Exact Rust paths changed
|
||||
|
||||
New:
|
||||
|
||||
- crates/oa-desktop/src/behavior/canvas_workspace.rs
|
||||
- crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs
|
||||
|
||||
Modified:
|
||||
|
||||
- crates/oa-desktop/src/behavior.rs
|
||||
- crates/oa-desktop/src/behavior/topology.rs
|
||||
- crates/oa-desktop/src/behavior/topology/direct.rs
|
||||
- crates/oa-desktop/src/behavior/scenario.rs
|
||||
- crates/oa-desktop/src/behavior/scenario/workspace.rs
|
||||
|
||||
This new handoff is the only report written in the implementation worktree. No App, core, source grammar, REST, schema, architecture/state implementation, existing tests, ledgers or prerequisites files were edited. The earlier design report was written in the original checkout under its prior report-only lease; do not confuse that completed handoff with this interrupted implementation.
|
||||
|
||||
## Saved implementation so far
|
||||
|
||||
- New shared module defines Scope (document/owner/revision), relative Pan and cursor-anchored Zoom events, Camera change helper, a constant-width tooltip palette, and a stable header/canvas/palette host with an overlaid editor. It contains no model commands.
|
||||
- New generic Iced Viewport wrapper implements focusable state, ordered press-position retention, middle/Space-drag, focused arrow camera keys, wheel zoom, focus border, input capture, child rendering clipped to viewport, and delegated widget operations/overlays. No tests or compiler validated its API usage.
|
||||
- Behavior declares canvas_workspace. Scene and Graph have a default-zero camera_pan, frame translation and inverse-hit coordinate changes. Existing scroll_offset remains for transitional compatibility; current production views still set/use old scroll offsets.
|
||||
- Scenario Presentation and process State have a new transient pan. Both expose camera() using the shared relative-input helper. Process enter resets pan with owner/document.
|
||||
- Both CanvasEvent enums gained Camera(Context, Event), and context() covers it.
|
||||
- Scenario whole-glyph reorder starts a captured Reorder for a hit occurrence with a row, preserving current-parent slot validation. The current interrupted code selects an unmoved occurrence on release; this was being reviewed against existing press-selection expectations. No process free-placement behavior was added; process semantic SetSteps remains explicitly constrained.
|
||||
|
||||
## Known incomplete work / blockers
|
||||
|
||||
1. canvas_workspace.rs declares mod tests, but **canvas_workspace/tests.rs does not exist**. Create meaningful mounted tests, not an empty placeholder. No existing test assertions were weakened or changed.
|
||||
2. New Camera variants have **no App handlers**; exhaustive matches may fail. Add minimal guarded adapters in App ScenarioCanvas and app/process_canvas.rs, plus Focus/Traverse Tasks. Relative camera events need document/owner/revision validation and correct same-batch ordering; root modal/pending guards must remain intact.
|
||||
3. Production process/scenario view functions have **not** been migrated to Fill viewport, the new palette or the stable overlay host. They still use model-sized canvases/Scrollables and split forms. Thus the current source does not deliver phase 1.
|
||||
4. Pan fields are not yet passed from presentation state into Scene/Graph, and old Fit/manual zoom paths are not reconciled with camera pan. Need explicit Fit reset/recenter and no automatic refit after editor/selection changes.
|
||||
5. The new Viewport's CursorMoved branch stores the event position, but its subsequent local pointer choice can still prefer Iced's final batch cursor. Correct this before testing; actual event positions must win for ordered movement.
|
||||
6. Starting camera input should invalidate an in-progress semantic gesture even with zero pan distance. A planned CancelGesture camera intent / epoch bump is not written. Camera input across batches must not lose an active pan solely because its own epoch changed; semantic releases must reject stale contexts.
|
||||
7. Scope widget focus identity currently omits revision. Review delayed Focus Tasks against replaced modal/document/revision state. Pending structured editors must own keyboard input; a retained background canvas focus must not leak arrows/Space into camera actions.
|
||||
8. Scenario caller frames already clone Presentation, so they can retain the new pan. Current ScrollRestore receipt machinery still targets the old Canvas Scrollable; migrate actual canvas restoration coherently while preserving Page/outline restoration, exact caller identity, one-shot behavior and raw guards. Process caller restoration/reveal currently targets Scrollable and also needs camera adaptation.
|
||||
9. Existing scenario FormShown/name focus, one-time marker reveal and staged child-source editor must survive the new stable editor presentation. Keep the same Draft and source_draft state; do not create a second draft, implicit Apply or substitute definitions.
|
||||
10. Existing workspace/scroll tests target old hosts. Replace obsolete presentation expectations with production-mounted canvas assertions while retaining all semantic/source/history/identity expectations. Do not leave legacy helper-only tests represented as new production acceptance.
|
||||
|
||||
## Next bounded work after authorization
|
||||
|
||||
Finish minimal BOTH-scene Fill/palette/editor adapters, guarded camera App events, stable caller return and whole-glyph scenario reorder. Exercise real Iced root layout and ordered pointer/key batches at nonzero pan and several zooms, frozen canvas bounds across edit/cancel, stale scope/release rejection, modal keyboard ownership, and exact App source/history/raw preservation. Send a coherent source freeze to root for compile and native gates; this worker remains prohibited from running Cargo/native/Git.
|
||||
|
||||
No persistent layout map, core/view schema extension, range wrapping, reconnection expansion, new allocation semantics or B4 changes belong in this slice. Root is compiling the original combined c0e66f9 source independently; do not modify that checkout or equate its results with this WIP. The incoming source_draft essentials-first fix is owned by Codex-9e8r6t; B4 renderer by Codex-ytr83r. Phase 1 is not issue #2 completion.
|
||||
|
||||
## Tool outcome
|
||||
|
||||
The first mechanical edit command attempted plain python and failed because python was not on PATH; it changed no files. The same intended edits were then applied successfully with the bundled Python at C:/Users/User/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe. Shared new files were written through apply_patch. No compile, formatter, unit test or native acceptance has run on these changes.
|
||||
Reference in New Issue
Block a user