git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

src/pages/mod.rs (23.9K)

  1 pub mod browse;
  2 pub mod preview;
  3 pub mod network;
  4 pub mod space;
  5 
  6 use cce_ui::widget::Handle;
  7 use cce_ui::layout::RenderTarget;
  8 
  9 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 10 pub enum Page {
 11     Browse,
 12     Network,
 13     Space,
 14 }
 15 
 16 impl Page {
 17     pub const ALL: [Page; 3] = [
 18         Page::Browse,
 19         Page::Network,
 20         Page::Space,
 21     ];
 22 
 23     pub fn label(self) -> &'static str {
 24         match self {
 25             Page::Browse => "Browse",
 26             Page::Network => "Network",
 27             Page::Space => "Space",
 28         }
 29     }
 30 
 31     /// The page's row in the breadcrumb's view menu. These name the
 32     /// visualization rather than the page, so they are not [`Self::label`].
 33     pub fn view_label(self) -> &'static str {
 34         match self {
 35             Page::Browse => "List",
 36             Page::Network => "Graph",
 37             Page::Space => "Space",
 38         }
 39     }
 40 }
 41 
 42 /// The breadcrumb's height on every page. Its segments are button plates, so
 43 /// it is one button tall: the toolkit's control height (as the widget's own
 44 /// `intrinsic_size` says), not a number of this crate's.
 45 pub fn breadcrumb_h() -> f32 {
 46     cce_ui::layout::button_height()
 47 }
 48 
 49 /// Lay out, render and carve a page's breadcrumb: the pane's full width along
 50 /// its top edge, the same on every page, so switching views leaves it where it
 51 /// was. Returns the top of the content under it, one plate gap below.
 52 ///
 53 /// Network and Space used to place theirs 4px in, 6px down and 16px narrower
 54 /// than Browse, an inset left over from before the pane rect carried it, and
 55 /// started their content 28px and 34px down against Browse's 24 + gap.
 56 pub fn breadcrumb_header(
 57     pc: &mut PageContent,
 58     breadcrumb: Handle<cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>>,
 59     cx: f32,
 60     cy: f32,
 61     cw: f32,
 62     ctx: &mut cce_ui::context::UiContext,
 63 ) -> f32 {
 64     let h = breadcrumb_h();
 65     cce_ui::layout::render_widget_h(pc, breadcrumb, cx, cy, cw, h, ctx);
 66     let rect = cce_ui::scene::layout::Rect { x: cx, y: cy, width: cw, height: h };
 67     breadcrumb_relief(pc, &ctx[breadcrumb], rect);
 68     cy + h + cce_ui::layout::root_plate_gap()
 69 }
 70 
 71 /// Mirror the breadcrumb's relief into a flat-path [`PageContent`]: the
 72 /// full-width recessed well, the ONE raised plate the segment run shares, and a
 73 /// slanted seam engraved at each boundary between two segments.
 74 ///
 75 /// `render_widget` drops the relief prims `Breadcrumb::paint` emits, so every
 76 /// page that shows a breadcrumb has to carve it app-side — this is that carve,
 77 /// in one place, since all three pages want it identically.
 78 ///
 79 /// **Audited against `Breadcrumb::paint` and deliberately left page-side** —
 80 /// it does NOT need the treatment the view dropdown's ring needed (carved into
 81 /// `window_pc` after the pages had laid it out; the dropdown is gone since
 82 /// 2026-10-06, its List/Graph/Space rows moved into this breadcrumb's context
 83 /// menu), for two reasons that are easy to assume away:
 84 ///
 85 /// - *The depths already agree.* Each `relief_*`/`groove` helper derives depth
 86 ///   from the height it is handed, and all three here are handed what the
 87 ///   widget uses: the well from `rect.height`, the plate from `rh`, and the
 88 ///   seams from the run as host — the widget engraves the seams at the RUN's
 89 ///   depth, not the well's. Nothing is pre-expanded, so nothing drifts the way
 90 ///   the dropdown's ring did.
 91 /// - *The rect is fresh.* Every caller builds it as a literal on the line after
 92 ///   laying the breadcrumb out, rather than reading it back off the widget, so
 93 ///   there is no previous-frame rect to pick up.
 94 ///
 95 /// Nor does the `window_pc`-vs-`pc` split matter here, though it did for the
 96 /// dropdown's ring. `display_list` emits ALL of a `PageContent`'s rects
 97 /// before ALL of its reliefs, so the call order within `pc` is irrelevant; only
 98 /// a different PageContent could reorder these. The only quad the breadcrumb
 99 /// puts under the carve is the hover tint, and that is inset to the seam's
100 /// furthest lean by construction, so it does not overlap the grooves. Moving
101 /// this carve to `window_pc` would buy nothing.
102 pub fn breadcrumb_relief(
103     pc: &mut PageContent,
104     breadcrumb: &cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>,
105     rect: cce_ui::scene::layout::Rect,
106 ) {
107     let r = cce_ui::layout::dropdown_corner_radius();
108     let Some(run) = breadcrumb.run_box(rect) else { return };
109     let (rx, ry, rw, rh) = run;
110     // The dropdown's flush inset plate on the segment run (mirroring
111     // Breadcrumb::paint's relief branch) — the full-rect recessed well and
112     // the raised run inside it are gone with the restyle.
113     //
114     // Face AND ring, through the `inset_plate` bridge, because the face is
115     // the dropdown's configured fill: `render_widget` offers that fill for a
116     // Dropdown through a per-type hook (layout.rs) but has no Breadcrumb arm,
117     // so a run carved here with no face would keep showing the window plate
118     // while the DE's dropdowns went opaque. It reads
119     // `dropdown_background_color` too, so they match under any config —
120     // transparent leaves the plate as the face.
121     let radius = r.min(rh * 0.5);
122     let depth = cce_ui::layout::bevel_width().min(rh * 0.2);
123     let raw = cce_ui::color::dropdown_background_color();
124     let face = if raw[3] > 0.001 {
125         let mut c = raw;
126         c[3] = 1.0;
127         c
128     } else {
129         [0.0; 4]
130     };
131     pc.inset_plate(face, rx, ry, rw, rh, radius, depth);
132     for (a, b) in breadcrumb.seams(rect) {
133         pc.groove(a, b, cce_ui::widget::Breadcrumb::SEAM_WIDTH, run);
134     }
135 }
136 
137 pub const RELIEF_RECESSED: u8 = 0;
138 pub const RELIEF_RAISED: u8 = 1;
139 pub const RELIEF_INSET: u8 = 2;
140 /// A recessed well with pointer focus: renders as the tinted carve — the
141 /// wrapped accent glint REPLACING the relief lighting (the DE's one focus
142 /// language, same treatment as a focused plate's ring).
143 pub const RELIEF_RECESSED_FOCUS: u8 = 3;
144 /// [`RELIEF_INSET`] with keyboard focus: the flush plate's rim lit in the
145 /// highlight — the ring a focused control plate wears (`ControlPlate::with_tint`).
146 pub const RELIEF_INSET_FOCUS: u8 = 4;
147 
148 /// A stroked shape a widget draws: a graph's wire (a line, or an arc at a
149 /// rounded bend) or its port (a disc). Coordinates as `PaintCtx` takes them;
150 /// an arc's `radius` is its OUTER edge.
151 #[derive(Debug, Clone, Copy, PartialEq)]
152 pub enum Stroke {
153     Line { x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, cap: cce_ui::scene::paint::Cap },
154     Arc { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32 },
155     Disc { cx: f32, cy: f32, radius: f32 },
156 }
157 
158 impl Stroke {
159     /// The shape's bounding box `(x, y, w, h)` — what culls it.
160     pub fn bounds(&self) -> (f32, f32, f32, f32) {
161         match *self {
162             Stroke::Line { x1, y1, x2, y2, thickness, .. } => {
163                 let h = thickness * 0.5;
164                 (x1.min(x2) - h, y1.min(y2) - h, (x1 - x2).abs() + thickness, (y1 - y2).abs() + thickness)
165             }
166             Stroke::Arc { cx, cy, radius, .. } | Stroke::Disc { cx, cy, radius } => {
167                 (cx - radius, cy - radius, 2.0 * radius, 2.0 * radius)
168             }
169         }
170     }
171 }
172 
173 pub struct PageContent {
174     pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
175     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
176     pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, crate::Message)>,
177     /// Relief steps for the control_relief styling — (x, y, w, h, radius, depth,
178     /// kind: [`RELIEF_RECESSED`]/[`RELIEF_RAISED`]/[`RELIEF_INSET`]). The flat rects
179     /// own the faces; these are the edges-only walls emitted over them (the
180     /// ParametersBg::reliefs idiom for flat-view hosts).
181     pub reliefs: Vec<(f32, f32, f32, f32, f32, f32, u8)>,
182     /// Engraved lines over the flat rects — (ax, ay, bx, by, width, depth) plus
183     /// the (x, y, w, h) of the surface being engraved, which the shading fades
184     /// out against. Unlike [`reliefs`] these are not axis-aligned: this is the
185     /// breadcrumb's slanted segment seams.
186     ///
187     /// [`reliefs`]: PageContent::reliefs
188     pub grooves: Vec<(f32, f32, f32, f32, f32, f32, f32, f32, f32, f32)>,
189     /// GPU-textured quads — (image id from `cce_ui::vk::upload_rgba`, x, y, w, h,
190     /// alpha). Drawn after the part's rects, so a fill emitted earlier is the floor
191     /// beneath the image and overlay parts still cover it.
192     pub images: Vec<(u32, f32, f32, f32, f32, f32)>,
193     /// cce-icons glyphs — (name, x, y, w, h, colour, clip `[l, t, r, b]`).
194     /// The colour is a text colour (raw sRGB, alpha = opacity), so a glyph and
195     /// the label beside it match; drawn through `PaintCtx::icon` in the part's
196     /// order, after its images. The clip cuts a glyph scrolled half under an
197     /// edge rather than squashing it, as text bounds cut a label.
198     pub icons: Vec<(String, f32, f32, f32, f32, [f32; 4], Option<[f32; 4]>)>,
199     /// Strokes, arcs and discs — (shape, colour, clip `[l, t, r, b]`): a
200     /// widget's wires and ports as `render_widget` hands them over
201     /// (`RenderTarget::line` / `arc` / `circle`). The graph page's wires.
202     pub strokes: Vec<(Stroke, [f32; 4], Option<[f32; 4]>)>,
203     /// Lit plates — (color, x, y, w, h, radius, depth). Unlike [`reliefs`], a plate
204     /// owns its FILL as well as its edge: one primitive carrying a rounded face and
205     /// the rolled, lit perimeter, shaded in a single lighting evaluation. That is
206     /// what the pane plates and the preview stub wear, so a floating surface
207     /// (the context menu) reads as the same material rather than as a flat chip
208     /// inside a drawn frame.
209     ///
210     /// [`reliefs`]: PageContent::reliefs
211     pub plates: Vec<([f32; 4], f32, f32, f32, f32, f32, f32)>,
212 }
213 
214 impl PageContent {
215     pub fn new() -> Self {
216         Self {
217             rects: Vec::new(),
218             texts: Vec::new(),
219             buttons: Vec::new(),
220             reliefs: Vec::new(),
221             grooves: Vec::new(),
222             images: Vec::new(),
223             icons: Vec::new(),
224             strokes: Vec::new(),
225             plates: Vec::new(),
226         }
227     }
228 
229     /// Move every part of `other` into this content.
230     ///
231     /// Field-complete by construction: this replaces four hand-listed runs of
232     /// `self.x.extend(other.x)` in `rebuild_layout`, which silently dropped any
233     /// vec nobody remembered to add a line for — `grooves` was invisible for
234     /// exactly that reason. The destructuring below turns a new field into a
235     /// compile error instead of a missing mark on screen.
236     pub fn absorb(&mut self, other: PageContent) {
237         let PageContent { rects, texts, buttons, reliefs, grooves, images, icons, strokes, plates } = other;
238         self.rects.extend(rects);
239         self.texts.extend(texts);
240         self.buttons.extend(buttons);
241         self.reliefs.extend(reliefs);
242         self.grooves.extend(grooves);
243         self.images.extend(images);
244         self.icons.extend(icons);
245         self.strokes.extend(strokes);
246         self.plates.extend(plates);
247     }
248 
249     /// This content cut to `[l, t, r, b]`: fills intersected with it (a
250     /// fill wholly outside is dropped), and the bounds of every label and
251     /// glyph narrowed to it. For a widget that paints past the rect it was
252     /// given — the graph, panned — since a flat host has no clip stack.
253     /// Strokes are cut by their clip as glyphs are. Parts with no clip of
254     /// their own (buttons, reliefs, grooves, images,
255     /// plates) pass through as they are.
256     pub fn clipped_to(mut self, clip: [f32; 4]) -> Self {
257         let [l, t, r, b] = clip;
258         self.rects.retain_mut(|(_, x, y, w, h, _, _)| {
259             let (x0, y0) = (x.max(l), y.max(t));
260             let (x1, y1) = ((*x + *w).min(r), (*y + *h).min(b));
261             if x1 <= x0 || y1 <= y0 {
262                 return false;
263             }
264             (*x, *y, *w, *h) = (x0, y0, x1 - x0, y1 - y0);
265             true
266         });
267         let narrow = |bounds: Option<[f32; 4]>| -> Option<[f32; 4]> {
268             Some(match bounds {
269                 Some([bl, bt, br, bb]) => [bl.max(l), bt.max(t), br.min(r), bb.min(b)],
270                 None => clip,
271             })
272         };
273         for t in &mut self.texts {
274             t.6 = narrow(t.6);
275         }
276         for i in &mut self.icons {
277             i.6 = narrow(i.6);
278         }
279         for st in &mut self.strokes {
280             st.2 = narrow(st.2);
281         }
282         self
283     }
284 
285     /// A GPU-textured quad (id from `cce_ui::vk::upload_rgba`).
286     pub fn image(&mut self, id: u32, x: f32, y: f32, w: f32, h: f32, alpha: f32) {
287         self.images.push((id, x, y, w, h, alpha));
288     }
289 
290     /// A cce-icons glyph (`folder`, `file-image`, …) at `(x, y, w, h)`, tinted
291     /// `color` as a label is — the one way this crate draws a symbol.
292     pub fn icon(&mut self, name: &str, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) {
293         self.icons.push((name.to_string(), x, y, w, h, color, None));
294     }
295 
296     /// [`PageContent::icon`] cut to `bounds` `[l, t, r, b]`.
297     pub fn icon_bounded(&mut self, name: &str, x: f32, y: f32, w: f32, h: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
298         self.icons.push((name.to_string(), x, y, w, h, color, bounds));
299     }
300 
301     pub fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
302         self.rects.push((color, x, y, w, h, 0.0, (true, true, true, true)));
303     }
304 
305     /// [`PageContent::rect`] with a corner radius, applied only to `corners`
306     /// (top-left, top-right, bottom-right, bottom-left) — for a fill that has to
307     /// follow the rounded corner of the plate it sits inside.
308     pub fn rect_rounded(
309         &mut self,
310         color: [f32; 4],
311         x: f32,
312         y: f32,
313         w: f32,
314         h: f32,
315         radius: f32,
316         corners: (bool, bool, bool, bool),
317     ) {
318         self.rects.push((color, x, y, w, h, radius, corners));
319     }
320 
321     /// A lit plate at (x, y, w, h): a rounded face in `color` plus the rolled,
322     /// lit perimeter — the treatment the pane plates wear. Unlike the relief
323     /// helpers this is NOT gated on `control_relief`: a plate owns the fill, so
324     /// skipping it would leave the surface unpainted rather than merely flat.
325     pub fn plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
326         let depth = cce_ui::layout::bevel_width().min(h * 0.2);
327         self.plates.push((color, x, y, w, h, radius, depth));
328     }
329 
330     /// Wall width for a carve whose corners are rounded at `radius`, capped so
331     /// the wall stays crease-free through the corner. A carve's wall straddles
332     /// the boundary, reaching `depth / 2` inward — and the inward offsets of a
333     /// squircle corner kink into a square crease past the corner's diagonal
334     /// curvature radius (`radius / corner_span_factor()`). The plate path
335     /// avoids this by widening its corner span instead, which a carve cannot:
336     /// its silhouette must stay on the widget's own nominal-radius corner.
337     /// Only tall rects ever feel the cap (a control's `h * 0.2` already lands
338     /// under it); the list well is the case that motivated it. Zero radius is
339     /// exempt — square corners meet in a miter by design.
340     fn carve_depth(h: f32, radius: f32) -> f32 {
341         let depth = cce_ui::layout::bevel_width().min(h * 0.2);
342         if radius > 0.0 {
343             depth.min(2.0 * radius / cce_ui::layout::corner_span_factor())
344         } else {
345             depth
346         }
347     }
348 
349     /// A recessed well carved over the control at (x, y, w, h) — no-op when the
350     /// DE's control_relief styling is off.
351     pub fn relief_recessed(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
352         if cce_ui::layout::control_relief() {
353             let depth = Self::carve_depth(h, radius);
354             self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED));
355         }
356     }
357 
358     /// [`Self::relief_recessed`] for the well holding pointer focus: the ring
359     /// replaces the lighting (see [`RELIEF_RECESSED_FOCUS`]).
360     pub fn relief_recessed_focused(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
361         if cce_ui::layout::control_relief() {
362             let depth = Self::carve_depth(h, radius);
363             self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED_FOCUS));
364         }
365     }
366 
367     /// A raised plateau over the control at (x, y, w, h) — no-op when the DE's
368     /// control_relief styling is off.
369     pub fn relief_raised(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
370         if cce_ui::layout::control_relief() {
371             let depth = Self::carve_depth(h, radius);
372             self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RAISED));
373         }
374     }
375 
376     /// A line engraved from `a` to `b` into the surface `host` — no-op when the
377     /// DE's control_relief styling is off.
378     pub fn groove(&mut self, a: (f32, f32), b: (f32, f32), width: f32, host: (f32, f32, f32, f32)) {
379         if cce_ui::layout::control_relief() {
380             let depth = cce_ui::layout::bevel_width().min(host.3 * 0.2);
381             self.grooves.push((a.0, a.1, b.0, b.1, width, depth, host.0, host.1, host.2, host.3));
382         }
383     }
384 
385     /// A flush inset button plate (groove ring + beveled lip, face level with the
386     /// surface — the Button treatment) over the control at (x, y, w, h) — no-op
387     /// when the DE's control_relief styling is off.
388     pub fn relief_inset(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
389         if cce_ui::layout::control_relief() {
390             let depth = Self::carve_depth(h, radius);
391             self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET));
392         }
393     }
394 
395     pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
396         self.texts.push((content.to_string(), size, x, y, color, None, None));
397     }
398 
399     pub fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
400         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
401     }
402 
403     /// `text_with_font` with an explicit clip box `[l, t, r, b]` — for text
404     /// that scrolls under an edge and must render cut, not culled.
405     pub fn text_with_font_bounded(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: [f32; 4]) {
406         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), Some(bounds)));
407     }
408 
409     pub fn button(
410         &mut self,
411         label: &str,
412         x: f32,
413         y: f32,
414         w: f32,
415         h: f32,
416         bg: [f32; 4],
417         hover_bg: [f32; 4],
418         label_color: [f32; 4],
419         action: crate::Message,
420     ) {
421         let btn = cce_ui::widget::Button::new(x, y, w, h)
422             .with_label(label)
423             .with_bg(bg)
424             .with_hover_bg(hover_bg)
425             .with_label_color(label_color);
426         self.buttons.push((btn, action));
427     }
428 
429     /// A button wearing the toolkit's own face — no per-call colors. The
430     /// hand-tinted variant above predates the themed Button; chrome buttons
431     /// (the chooser's Cancel/Save) should look like every other DE button.
432     pub fn button_plain(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32, action: crate::Message) {
433         // Colorless chrome: transparent face over the theme's border/relief —
434         // the closed-dropdown convention — with a faint neutral hover. The
435         // toolkit's Primary face is itself blue-tinted, which is exactly what
436         // these buttons are not supposed to be.
437         let btn = cce_ui::widget::Button::new(x, y, w, h)
438             .with_label(label)
439             .with_bg([0.0, 0.0, 0.0, 0.0])
440             .with_hover_bg([1.0, 1.0, 1.0, 0.10]);
441         self.buttons.push((btn, action));
442     }
443 
444     pub fn button_left(
445         &mut self,
446         label: &str,
447         x: f32,
448         y: f32,
449         w: f32,
450         h: f32,
451         bg: [f32; 4],
452         hover_bg: [f32; 4],
453         label_color: [f32; 4],
454         action: crate::Message,
455     ) {
456         let btn = cce_ui::widget::Button::new(x, y, w, h)
457             .with_label(label)
458             .with_bg(bg)
459             .with_hover_bg(hover_bg)
460             .with_label_color(label_color)
461             .with_left_align(true);
462         self.buttons.push((btn, action));
463     }
464 }
465 
466 impl RenderTarget for PageContent {
467     fn icon(&mut self, name: &str, rect: cce_ui::scene::layout::Rect, color: [f32; 4]) {
468         PageContent::icon(self, name, rect.x, rect.y, rect.width, rect.height, color);
469     }
470 
471     fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: cce_ui::scene::paint::Cap) {
472         self.strokes.push((Stroke::Line { x1, y1, x2, y2, thickness, cap }, color, None));
473     }
474 
475     fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
476         self.strokes.push((Stroke::Arc { cx, cy, radius, thickness, start, end }, color, None));
477     }
478 
479     fn circle(&mut self, cx: f32, cy: f32, radius: f32, color: [f32; 4]) {
480         self.strokes.push((Stroke::Disc { cx, cy, radius }, color, None));
481     }
482 
483     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
484         self.rects.push((color, x, y, w, h, 0.0, (true, true, true, true)));
485     }
486 
487     fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
488         self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
489     }
490 
491     fn rect_with_radius_corners(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, corners: (bool, bool, bool, bool)) {
492         self.rects.push((color, x, y, w, h, radius, corners));
493     }
494 
495     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
496         self.texts.push((content.to_string(), size, x, y, color, None, None));
497     }
498 
499     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
500         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
501     }
502 
503     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
504         self.texts.push((content.to_string(), size, x, y, color, None, bounds));
505     }
506 
507     fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
508         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
509     }
510 
511     /// The flat-path bridge for a widget's flush inset plate — the Dropdown's
512     /// OPEN popover, which is its trigger surface grown over the unified box.
513     /// Without this override the default degrades it to a plain rounded fill,
514     /// so the menu lost the groove ring and lip the closed trigger has the
515     /// moment it expanded.
516     ///
517     /// The face and the walls go to different vecs on purpose — `reliefs` are
518     /// edges-only, drawn over the faces `rects` own — and `display_list` emits
519     /// this part's rects before its reliefs, so one call here lands as fill
520     /// then ring, in that order, within whichever part is being collected.
521     ///
522     /// **The caller's `depth` is used verbatim, NOT re-derived from `h`.** The
523     /// widget computes it from the TRIGGER's height; the box handed here is the
524     /// trigger plus the revealed menu, several times taller. `relief_inset`
525     /// would recompute `bevel_width().min(h * 0.2)` off that expanded height
526     /// and thicken the ring as the menu grows. A ring that swells during the open
527     /// animation is exactly the artifact this override exists to avoid.
528     fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
529         self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
530         if cce_ui::layout::control_relief() {
531             self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET));
532         }
533     }
534     /// The focused control plate's ring — same plate, rim lit.
535     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, _tint: [f32; 3]) {
536         self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
537         if cce_ui::layout::control_relief() {
538             self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET_FOCUS));
539         }
540     }
541 }