Skip to main content

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}