git.lucas.co / cce-designer
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 }