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