graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/slots.rs (12.7K)
1 //! The widget roster: the app's top-level widgets in fixed, statically typed slots.
2 //!
3 //! The designer does not build a dynamic widget tree — every top-level pane, bar and
4 //! plate lives in a named field of [`WidgetSlots`], and the `*_IDX` constants address
5 //! those same fields positionally for the genuinely index-driven paths (draw order,
6 //! focus cycling, broadcast loops). The whole roster is declared once, in the
7 //! `widget_roster!` invocation below — one line per slot, from which the constants,
8 //! the struct fields and every index→field dispatch are generated. The typed accessors
9 //! that assert each slot's concrete type are hand-written, below the macro.
10
11 use cce_ui::widget::{
12 Adapted, Breadcrumb, Graph, ImageView, MenuBar, ParametersBg, Splitter, Spreadsheet,
13 StatusBar, WidgetHost,
14 };
15
16 use crate::playbar::Playbar;
17 use crate::viewport_3d::Viewport3D;
18
19 /// Declares the whole roster from one line per slot: `INDEX_CONST: field: WidgetType`.
20 ///
21 /// Declaration order *is* slot order: the `*_IDX` constants are numbered from it and
22 /// `WIDGET_COUNT` falls out of the length. The single list below generates the
23 /// constants, the `WidgetSlots` fields, and all four index→field dispatch matches, so
24 /// adding or reordering a pane is one line instead of six hand-kept edits whose only
25 /// backstop was a runtime panic.
26 macro_rules! widget_roster {
27 ($($idx:ident : $field:ident : $ty:ty),+ $(,)?) => {
28 widget_roster!(@number 0usize; $($idx)+);
29
30 /// The roster, concretely typed (Phase 6bb): every slot's type is statically known — the
31 /// old `Vec<Box<dyn WidgetHost>>` erased that and pinned `WidgetHost`'s full surface through the
32 /// broadcast loops. Boxed as a whole so registered widget pointers stay stable while the
33 /// containing `State` moves. The `*_IDX` constants keep addressing the same slots through
34 /// `get_dyn`/`get_dyn_mut` for the genuinely index-driven paths (draw order, focus cycling,
35 /// broadcast loops); everything else reaches the concrete field.
36 pub struct WidgetSlots {
37 $(pub $field: Adapted<$ty>,)+
38 }
39
40 impl WidgetSlots {
41 // Per-slot drag queries (the ControlPanel endgame took `draggable`/`is_dragging`
42 // off `WidgetHost`): the roster routes an index to the concrete slot's inherent
43 // `Adapted` read, like the other value drains.
44 pub fn draggable(&self, idx: usize) -> bool {
45 match idx {
46 $($idx => self.$field.draggable(),)+
47 _ => slot_out_of_range(idx),
48 }
49 }
50
51 pub fn is_dragging(&self, idx: usize) -> bool {
52 match idx {
53 $($idx => self.$field.is_dragging(),)+
54 _ => slot_out_of_range(idx),
55 }
56 }
57
58 pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
59 match idx {
60 $($idx => &self.$field,)+
61 _ => slot_out_of_range(idx),
62 }
63 }
64
65 pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
66 match idx {
67 $($idx => &mut self.$field,)+
68 _ => slot_out_of_range(idx),
69 }
70 }
71 }
72 };
73
74 // Number the constants in declaration order; what is left over at the end is the count.
75 (@number $n:expr;) => {
76 pub const WIDGET_COUNT: usize = $n;
77 };
78 (@number $n:expr; $head:ident $($rest:ident)*) => {
79 pub const $head: usize = $n;
80 widget_roster!(@number $n + 1; $($rest)*);
81 };
82 }
83
84 /// Every dispatch match needs a `_` arm — the compiler cannot see that the constant
85 /// patterns cover `0..WIDGET_COUNT` — and `!` fits all four return types.
86 #[cold]
87 #[inline(never)]
88 fn slot_out_of_range(idx: usize) -> ! {
89 panic!("widget slot index out of range: {idx}")
90 }
91
92 widget_roster! {
93 HEADER_IDX: header: MenuBar,
94 CONTENT_IDX: content: Graph,
95 SPLITTER1_IDX: splitter1: Splitter,
96 VIEWPORT_IDX: viewport: Viewport3D,
97 SPLITTER2_IDX: splitter2: Splitter,
98 PARAM_IDX: param: ParametersBg,
99 CANVAS_IDX: canvas: Canvas,
100 LEFT_MENUBAR_IDX: left_menubar: MenuBar,
101 RIGHT_MENUBAR_IDX: right_menubar: MenuBar,
102 PARAM_MENUBAR_IDX: param_menubar: MenuBar,
103 STATUS_IDX: status: StatusBar,
104 BREADCRUMB_IDX: breadcrumb: Breadcrumb,
105 SPREADSHEET_IDX: spreadsheet: Spreadsheet,
106 SPREADSHEET_MENUBAR_IDX: spreadsheet_menubar: MenuBar,
107 NETWORK_PANEL_IDX: network_panel: PassivePlate,
108 PLAYBAR_IDX: playbar: Playbar,
109 // The second network editor (plate + graph + breadcrumb): an independent
110 // VIEW of the same project with its own current path, created and placed
111 // through the plate corner menus' tab rows. Appended so the established
112 // slot indexes stay stable.
113 NETWORK_PANEL2_IDX: network_panel2: PassivePlate,
114 CONTENT2_IDX: content2: Graph,
115 BREADCRUMB2_IDX: breadcrumb2: Breadcrumb,
116 // The 2D page context's surface, sharing the viewport's rect and shown in
117 // its place when the displayed level holds a page. Appended, like the
118 // second network editor, so established slot indexes stay stable.
119 PAGE_IDX: page_view: ImageView,
120 // The Alt+D dialog: plate, query line and the one list of commands and
121 // settings, its controls painted from the toolkit's own stamps. Appended,
122 // like every slot since the second network editor, so established
123 // indexes stay stable. (A second ParametersBg slot for a Settings half
124 // sat after it until 2026-09-24.)
125 DIALOG_IDX: dialog: crate::dialog::Dialog,
126 }
127
128 impl WidgetSlots {
129 /// Roster index of the slot at `target_addr` (a thin widget address — the comparison
130 /// never dereferences; callers pass `ptr as *const ()`).
131 pub fn find_index(&self, target_addr: *const ()) -> Option<usize> {
132 (0..WIDGET_COUNT).position(|i| {
133 let w_ptr = self.get_dyn(i) as *const dyn WidgetHost as *const ();
134 w_ptr == target_addr
135 })
136 }
137
138 // Roster accessors on CONCRETE types (Phase 6aw, controller decision option 2): each
139 // index's type is known statically, so the capability traits are reached by downcast +
140 // Deref instead of WidgetHost's deleted as_*_controller discovery hooks. Signatures keep
141 // returning the narrow trait objects so the ~40 call sites stay unchanged. The dynamic
142 // `idx` of menu()/menu_mut() only ever receives the five menubar indexes.
143 pub fn viewport(&self) -> &Viewport3D {
144 self.viewport
145 .as_any()
146 .downcast_ref::<Viewport3D>()
147 .expect("VIEWPORT_IDX must be a Viewport3D")
148 }
149
150 pub fn viewport_mut(&mut self) -> &mut Viewport3D {
151 self.viewport
152 .as_any_mut()
153 .downcast_mut::<Viewport3D>()
154 .expect("VIEWPORT_IDX must be a Viewport3D")
155 }
156
157 /// The menu-capable roster entries are exactly the `Adapted<MenuBar>` bars (Phase 6aw
158 /// concrete typing); `None` for everything else.
159 pub fn menubar_at(&self, idx: usize) -> Option<&MenuBar> {
160 // Adapted::as_any exposes the INNER widget, so the downcast targets MenuBar itself.
161 self.get_dyn(idx).as_any().downcast_ref::<MenuBar>()
162 }
163
164 pub fn menu(&self, idx: usize) -> &dyn cce_ui::widget::MenuController {
165 self.get_dyn(idx).as_any().downcast_ref::<MenuBar>().expect("not a MenuBar")
166 }
167
168 pub fn menu_mut(&mut self, idx: usize) -> &mut dyn cce_ui::widget::MenuController {
169 self.get_dyn_mut(idx).as_any_mut().downcast_mut::<MenuBar>().expect("not a MenuBar")
170 }
171
172 pub fn graph(&self) -> &dyn cce_ui::widget::GraphController {
173 self.content.as_any().downcast_ref::<Graph>().expect("CONTENT_IDX must be a Graph")
174 }
175
176 pub fn graph_mut(&mut self) -> &mut dyn cce_ui::widget::GraphController {
177 self.content.as_any_mut().downcast_mut::<Graph>().expect("CONTENT_IDX must be a Graph")
178 }
179
180 pub fn param(&self) -> &dyn cce_ui::widget::ParamController {
181 self.param.as_any().downcast_ref::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
182 }
183
184 pub fn param_mut(&mut self) -> &mut dyn cce_ui::widget::ParamController {
185 self.param.as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
186 }
187
188 /// The pane as its concrete type, for what `ParamController` does not
189 /// carry — the code row's error line.
190 pub fn param_bg_mut(&mut self) -> &mut ParametersBg {
191 self.param.as_any_mut().downcast_mut::<ParametersBg>().expect("PARAM_IDX must be a ParametersBg")
192 }
193
194 pub fn spreadsheet_mut(&mut self) -> &mut dyn cce_ui::widget::SpreadsheetController {
195 self.spreadsheet.as_any_mut().downcast_mut::<Spreadsheet>().expect("SPREADSHEET_IDX must be a Spreadsheet")
196 }
197
198 pub fn path_mut(&mut self) -> &mut dyn cce_ui::widget::PathController {
199 self.breadcrumb.as_any_mut().downcast_mut::<Breadcrumb>().expect("BREADCRUMB_IDX must be a Breadcrumb")
200 }
201 }
202
203
204 /// Dissolved cce-ui `Plate` (Phase 6as): a passive panel wearing the parameter
205 /// plate's fill — same tint, opacity, and blur-behind marker (`param_plate_fill`),
206 /// so the network plate's bevel rolls exactly like the params plate's and tracks a
207 /// live retint/opacity/blur toggle. NOT scaled by the network fade (it was until
208 /// 2026-09-20): the graph's grid cells are this plate showing through, so the
209 /// plate has to be the pane material verbatim or the network pane reads as a
210 /// different colour from every other pane. The fade applies to what is drawn
211 /// over it — grid lines, wires, text. No children, no events.
212 pub struct PassivePlate {
213 /// Circular hit shape while the network pane is round (the legacy Plate marker).
214 curved_circle: Option<(f32, f32, f32)>,
215 }
216
217 impl PassivePlate {
218 pub fn new() -> cce_ui::widget::Adapted<PassivePlate> {
219 cce_ui::widget::Adapted::new(Self {
220 curved_circle: None,
221 })
222 }
223
224 pub fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
225 self.curved_circle = circle;
226 }
227 }
228
229 impl cce_ui::widget::Layout for PassivePlate {}
230
231 impl cce_ui::widget::Paint for PassivePlate {
232 /// Nothing: the plate's fill AND border are drawn by `append_widget_plate` (or, in
233 /// circular mode, the circle+arc branch) in the designer's hand-ordered paint walk.
234 /// The default `paint` would emit a plain square-cornered quad of the whole rect,
235 /// which the walk then re-drew through `extra_quads` ON TOP of the rounded plate —
236 /// square corners over the rounded ones.
237 fn paint(&self, _rect: cce_ui::scene::layout::Rect, _ctx: &mut cce_ui::scene::paint::PaintCtx) {}
238
239 fn color(&self) -> [f32; 4] {
240 // `param_plate_fill` folds in the plate opacity and the blur marker.
241 cce_ui::colors::param_plate_fill()
242 }
243
244 fn corner_style(&self, _rect: cce_ui::scene::layout::Rect) -> Option<(f32, (bool, bool, bool, bool))> {
245 let r = cce_ui::layout::plate_corner_radius();
246 let on = r > 0.0;
247 Some((r, (on, on, on, on)))
248 }
249
250 fn solid_border(&self) -> Option<([f32; 4], f32)> {
251 if let Some(bc) = cce_ui::colors::plate_border_color() {
252 Some((bc, cce_ui::colors::plate_border_thickness()))
253 } else {
254 None
255 }
256 }
257 }
258
259 impl cce_ui::widget::Input for PassivePlate {
260 fn hit(&self, rect: cce_ui::scene::layout::Rect, x: f32, y: f32) -> bool {
261 if let Some((cx, cy, r)) = self.curved_circle {
262 let dx = x - cx;
263 let dy = y - cy;
264 return dx * dx + dy * dy <= r * r;
265 }
266 // Exclusive right/bottom edges, like the legacy Plate hit test.
267 x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height
268 }
269 }
270
271
272 /// App-owned copy of the dissolved cce-ui `Canvas` (Phase 6ay part 2): the transparent
273 /// hit-through pane behind the network area. Verbatim; dies with the machinery retype.
274 pub struct Canvas;
275
276 impl Canvas {
277 pub fn new() -> cce_ui::widget::Adapted<Canvas> { cce_ui::widget::Adapted::new(Canvas) }
278 }
279
280 impl cce_ui::widget::Layout for Canvas {}
281
282 impl cce_ui::widget::Paint for Canvas {
283 fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
284 }
285
286 impl cce_ui::widget::Input for Canvas {
287 // Hit-through: the pane never claims the pointer (the graph decides its own hits).
288 fn hit(&self, _rect: cce_ui::scene::layout::Rect, _x: f32, _y: f32) -> bool { false }
289 }