blockworx_kernel/frame.rs
1//! One canvas frame, over whatever [`Canvas`] the host hands in.
2//!
3//! The on-screen shell and the kernel run *this* function; neither has a
4//! second copy. What is left outside it is only what the surface owns — the
5//! grid under the diagram, the widget the in-place editor is rendered as, and
6//! the easing a framing glides along.
7
8use blockworx_editor::{edit::describe::Label, widget::drawing::Drawing};
9use blockworx_geom::{Align2, Rect, WorldPx, grid::GRID_SIZE, vec2};
10use blockworx_paint::{
11 Canvas, Font, Interaction, Renderer,
12 theme::{Role, RoleStroke, Style},
13};
14use blockworx_router::turtle::Mark;
15use blockworx_tools::{
16 SelectTool,
17 tool::{Tool, ToolTrait, Transition},
18};
19
20use crate::session::{Diagnostic, Session};
21
22/// What one canvas frame left for the chrome around it.
23pub struct Framed {
24 /// What the tool asked for, if anything.
25 pub transition: Option<Transition>,
26 /// The selection's on-screen bounding box, for the overlay that anchors
27 /// to it. Taken inside the frame, where the world→screen transform and
28 /// the text metrics the pin bounds need are both in scope.
29 pub selection_bounds: Option<Rect>,
30}
31
32/// Draw debug marks from the routing engine. Coordinates and sizes are in
33/// world units; `Style` applies the zoom transform and world→screen mapping,
34/// so they pass through unscaled.
35fn draw_debug_marks(painter: &Style<'_, impl Renderer>, marks: &[Mark]) {
36 let font = Font::monospace(6.0);
37 for mark in marks {
38 match *mark {
39 Mark::Line { from, to } => {
40 painter.line_segment([from, to], (0.5, Role::DebugMark));
41 }
42 Mark::Circle { center, radius } => {
43 painter.circle(center, radius, Role::DebugMark, RoleStroke::NONE);
44 }
45 Mark::Label { value, pos } => {
46 painter.text(
47 pos,
48 Align2::CENTER_CENTER,
49 format!("{value:.1}"),
50 &font,
51 Role::DebugMark,
52 );
53 }
54 }
55 }
56}
57
58impl Session {
59 /// What a fit-to-content framing would frame: the extent of everything
60 /// this level's draw passes cover, measured through the backend that
61 /// would draw them.
62 pub fn content_bounds<R: Renderer>(&mut self, renderer: &mut R) -> Option<Rect> {
63 let Session {
64 doc,
65 time_machine,
66 doc_index,
67 spatial,
68 presentation,
69 gesture,
70 path,
71 theme,
72 ..
73 } = self;
74 let document = match time_machine {
75 None => doc.repo(),
76 Some(past) => past.repo(),
77 }
78 .document();
79 let index = spatial.get(doc_index, document, path, presentation);
80 let drawing =
81 Drawing::new_indexed(doc_index.view(document), path, index, presentation, gesture);
82 drawing.content_bounds(&Style::new(theme, renderer))
83 }
84
85 /// One canvas frame: the gesture bracket, the ring over what the last
86 /// step changed, the tool, and the escape that drops back to selection.
87 ///
88 /// The pass *is* the tool's gesture: everything written before the seal
89 /// at the end is one commit, labelled for the tool that made it. A pass
90 /// that only previewed writes nothing and seals to no commit.
91 pub fn canvas_frame<C: Canvas>(&mut self, interaction: &Interaction, canvas: &mut C) -> Framed {
92 self.begin_gesture(Label::verb(self.tool.name().verb()));
93 // Remember the world-space pointer position so a paste can drop the
94 // group under the cursor (hover preferred, else last click).
95 self.note_pointer(interaction.event);
96 let framed = {
97 let Session {
98 doc,
99 time_machine,
100 doc_index,
101 spatial,
102 presentation,
103 gesture,
104 path,
105 theme,
106 tool,
107 spotlight,
108 diagnostic,
109 ..
110 } = self;
111 let document = match time_machine {
112 None => doc.repo(),
113 Some(past) => past.repo(),
114 }
115 .document();
116 // Rebuilt here (not in a prior pass) so render and hit-tests share
117 // one index; a stale index can't be read — `get` rebuilds it if an
118 // edit invalidated it or the level changed.
119 let index = spatial.get(doc_index, document, path, presentation);
120 let mut drawing =
121 Drawing::new_indexed(doc_index.view(document), path, index, presentation, gesture);
122 let mut style = Style::new(theme, canvas);
123 // What the last document step changed, ringed for as long as the
124 // ring lasts. Drawn under the tool so a preview or a handle is
125 // never behind it.
126 spotlight.ring(path.scope(), &mut style);
127 let mut transition = {
128 let _span = tracing::info_span!("tool_widget").entered();
129 blockworx_tools::tool::frame(tool, &mut drawing, interaction, &mut style)
130 };
131 // Esc cancels the active tool, dropping back to plain selection —
132 // unless the tool consumed the key itself (a rename box
133 // commits/aborts its edit and sets its own action above; an
134 // in-progress route clears its preview).
135 if transition.is_none()
136 && interaction.escape_pressed
137 && !matches!(tool, Tool::Select(_))
138 {
139 transition = Some(Transition::SwitchTool(Tool::Select(SelectTool)));
140 }
141 if *diagnostic == Diagnostic::RegionalRouter {
142 let selected = tool
143 .selection()
144 .and_then(|d| d.shapes())
145 .unwrap_or_default();
146 let foreground = blockworx_editor::widget::foreground::Foreground::raising(
147 selected.iter().copied(),
148 &drawing,
149 );
150 blockworx_editor::widget::display::draw_foreground(
151 &drawing,
152 &foreground,
153 &mut style,
154 );
155 if let Some((bound, lattice)) = drawing.healing_bound(&foreground) {
156 draw_debug_marks(&style, &lattice);
157 style.rect(
158 bound,
159 WorldPx::ZERO,
160 Role::Transparent,
161 (1.5, Role::DebugMark),
162 );
163 }
164 }
165 let selection_bounds = tool.selection().and_then(|sel| {
166 // A tool may pin its overlay to a specific point (a route
167 // anchors at the click, not its wide bounding box); otherwise
168 // fall back to the selection's bounds.
169 let world = match tool.overlay_anchor() {
170 Some(anchor) => {
171 Some(Rect::from_center_size(anchor, vec2(GRID_SIZE, GRID_SIZE)))
172 }
173 None => blockworx_tools::selection_bounds::selection_world_bounds(
174 &sel, &drawing, &style,
175 ),
176 };
177 world.map(|world| style.remap_rect(world))
178 });
179 Framed {
180 transition,
181 selection_bounds,
182 }
183 };
184 self.end_gesture();
185 framed
186 }
187}