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