git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/layout/bridge.rs (27.3K)

  1 //! The flat-host render bridge: [`RenderTarget`], the target a host that does not build a
  2 //! display list paints into, [`render_widget`] / [`render_popovers`] which replay a widget
  3 //! onto one, and the carve and popover types that cross it. Legacy: new code paints
  4 //! through `scene::paint::PaintCtx`.
  5 
  6 use crate::widget::WidgetHostExt;
  7 use crate::widget::WidgetHost;
  8 use crate::context::UiContext;
  9 
 10 /// One step carve a widget's `paint` draws, handed to a flat-path host through
 11 /// [`RenderTarget::relief_carve`] so it can re-emit it as a real prim.
 12 ///
 13 /// Geometry always comes from the WIDGET (`TextBox::well`, `Toggle::well` /
 14 /// `Toggle::slide_plate`), never re-derived here — a second copy of that math
 15 /// in the bridge is exactly how the flat host's carve and the drawn one drift
 16 /// apart.
 17 #[derive(Clone, Copy, Debug, PartialEq)]
 18 pub struct ReliefCarve {
 19     pub kind: CarveKind,
 20     pub x: f32,
 21     pub y: f32,
 22     pub w: f32,
 23     pub h: f32,
 24     /// Per-corner radii, clockwise from top-left.
 25     pub radii: (f32, f32, f32, f32),
 26     /// Full width of the step's transition band.
 27     pub depth: f32,
 28     /// Which walls the carve has (top, right, bottom, left). A suppressed wall
 29     /// means the step runs flush to its neighbour there — a Spinbox's field
 30     /// running into its button column, where the two meet in ONE step rather
 31     /// than two facing walls.
 32     pub edges: (bool, bool, bool, bool),
 33 }
 34 
 35 /// Which way a [`ReliefCarve`] steps.
 36 #[derive(Clone, Copy, Debug, PartialEq)]
 37 pub enum CarveKind {
 38     /// Interior one step DOWN ([`crate::scene::paint::PaintCtx::recess_edges`]).
 39     /// `tint` lights the rim in the focus accent (`recess_tinted`).
 40     Recess { tint: Option<[f32; 3]> },
 41     /// Interior one step UP ([`crate::scene::paint::PaintCtx::boss_edges`]).
 42     Boss { tint: Option<[f32; 3]> },
 43     /// A FLUSH inset ([`crate::scene::paint::PaintCtx::trough_edges`]): the
 44     /// interior stays level with the surface and a valley seam runs the
 45     /// boundary — the closed dropdown's chrome, for a control that is part
 46     /// of the plate rather than a step up or down from it.
 47     Trough,
 48 }
 49 
 50 impl ReliefCarve {
 51     /// This carve shifted vertically — the page-scroll adjustment a host
 52     /// applies when it re-emits collected carves.
 53     pub fn shifted_y(self, dy: f32) -> Self {
 54         Self { y: self.y + dy, ..self }
 55     }
 56 }
 57 
 58 pub trait RenderTarget {
 59     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
 60     fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, _radius: f32) {
 61         self.rect(color, x, y, w, h);
 62     }
 63     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)) {
 64         self.rect_with_radius(color, x, y, w, h, radius);
 65     }
 66     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]);
 67     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _font: &str) {
 68         self.text(content, x, y, size, color);
 69     }
 70     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _bounds: Option<[f32; 4]>) {
 71         self.text(content, x, y, size, color);
 72     }
 73     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]>) {
 74         self.text_with_font(content, x, y, size, color, font);
 75     }
 76     fn push_clip_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
 77     fn pop_clip_rect(&mut self) {}
 78     /// A bundled cce-icons glyph — see `PaintCtx::icon`. A target that
 79     /// cannot draw images (the legacy `PopoverCollector`) draws nothing.
 80     fn icon(&mut self, _name: &str, _rect: crate::scene::layout::Rect, _color: [f32; 4]) {}
 81     /// A straight stroke — see `PaintCtx::vector`. What a graph's wires are
 82     /// made of; a target that draws none (the default) shows no wires.
 83     fn line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _thickness: f32, _color: [f32; 4], _cap: crate::scene::paint::Cap) {}
 84     /// A stroked arc, `radius` its OUTER edge — see `PaintCtx::arc`. A
 85     /// rounded wire's bends.
 86     fn arc(&mut self, _cx: f32, _cy: f32, _radius: f32, _thickness: f32, _start: f32, _end: f32, _color: [f32; 4]) {}
 87     /// A filled disc — see `PaintCtx::circle`. A graph's ports.
 88     fn circle(&mut self, _cx: f32, _cy: f32, _radius: f32, _color: [f32; 4]) {}
 89     /// A flush inset control plate ([`PaintCtx::inset_plate`]) — the raised
 90     /// control surface (groove ring down, beveled lip back up). Lets a popover
 91     /// draw the ACTUAL widget surface expanded (the Dropdown's grown trigger).
 92     /// Hosts without relief prims degrade to a flat rounded fill.
 93     fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, _depth: f32) {
 94         self.rect_with_radius(color, x, y, w, h, radius);
 95     }
 96     /// [`inset_plate`](Self::inset_plate) with the rim lit — the focused
 97     /// control plate's ring (`ControlPlate::with_tint`). Hosts without relief
 98     /// prims draw the plain plate.
 99     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, _tint: [f32; 3]) {
100         self.inset_plate(color, x, y, w, h, radius, depth);
101     }
102     /// One step carve from a widget's `paint` ([`ReliefCarve`]) — offered here
103     /// for the same reason as `inset_plate`: the legacy `all_quads` stream
104     /// carries no relief prims, so a flat-path host never sees them.
105     ///
106     /// The default is deliberately a NO-OP, not a fill. These controls have
107     /// transparent faces by design (the host surface IS the well floor / the
108     /// plate's face), so the carve is their entire decoration — a host that
109     /// can't carve has nothing truthful to draw, and a solid box here would
110     /// paint every text field a flat slab it never had.
111     fn relief_carve(&mut self, _carve: &ReliefCarve) {}
112     /// Whether this host renders sections as sunken wells (the designer idiom).
113     /// `SectionContext` then lays the title out left-aligned over its tab box
114     /// instead of centered on the top border.
115     fn section_relief_style(&self) -> bool {
116         false
117     }
118     /// The section frame hatch: `SectionContext::finish` offers the frame here
119     /// before falling back to the legacy 1px outline. A relief-capable host
120     /// returns true and carves the section into its plate instead (the
121     /// designer sunken-well idiom); the tuple hosts keep the default.
122     fn section_relief(&mut self, _frame: &SectionFrame) -> bool {
123         false
124     }
125 }
126 
127 /// A section frame offered to [`RenderTarget::section_relief`]: the content
128 /// body box plus, under [`RenderTarget::section_relief_style`], the title tab
129 /// box the label was laid out in — the tab sits flush on the body's top edge
130 /// (the designer union-carve shape).
131 pub struct SectionFrame {
132     pub x: f32,
133     pub y: f32,
134     pub w: f32,
135     pub h: f32,
136     pub tab: Option<(f32, f32, f32, f32)>,
137     pub focused: bool,
138     pub is_child: bool,
139 }
140 
141 pub struct PopoverCollector {
142     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
143     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
144 }
145 
146 impl PopoverCollector {
147     pub fn new() -> Self {
148         Self { rects: Vec::new(), texts: Vec::new() }
149     }
150 }
151 
152 impl RenderTarget for PopoverCollector {
153     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
154         self.rects.push((color, x, y, w, h));
155     }
156 
157     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
158         self.texts.push((content.to_string(), size, x, y, color, None, None));
159     }
160 
161     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
162         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
163     }
164 
165     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
166         self.texts.push((content.to_string(), size, x, y, color, None, bounds));
167     }
168 
169     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]>) {
170         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
171     }
172 }
173 
174 
175 /// [`render_widget`] for a widget the context owns, named by its handle: lent for the call.
176 /// Draws nothing if the widget is gone or already out on loan.
177 pub fn render_widget_h<T: WidgetHost + 'static>(
178     pc: &mut dyn RenderTarget,
179     h: crate::widget::Handle<T>,
180     x: f32,
181     y: f32,
182     ww: f32,
183     wh: f32,
184     ctx: &mut UiContext,
185 ) {
186     ctx.lend_h(h, |w, ctx| render_widget(pc, w, x, y, ww, wh, ctx));
187 }
188 
189 pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
190     // The flat-host contract, the same block `set_rect` takes: `(x, y)` is the top of
191     // the detached label and `wh` the block height, label strip included. `layout`
192     // takes the CONTENT origin and height, so step down by the strip.
193     let strip = w.label_strip();
194     let content_h = (wh - strip).max(0.0);
195     w.layout(crate::widget::Point { x, y: y + strip }, crate::widget::LayoutConstraints::new(ww, ww, content_h, content_h), ctx);
196 
197     // Shape, which on this path nobody else does. A flat host consumes
198     // `all_quads`, so `prepare_text` — where a TextBox records the per-glyph x
199     // offsets its selection highlight, caret and click->index mapping all read
200     // — was never called for the widgets it draws. Those three then fell back
201     // to `measure_text_width("M")`, an SVG-rasterized INKED extent rather than
202     // an advance, so the highlight under-ran the glyphs by a few px per
203     // character (a full glyph by the end of "example.com"). Hosts that shape
204     // for themselves (cce-files, the TreeList) just re-read the shared buffer
205     // cache here.
206     if let Ok(mut fs) = crate::geometry_font_system().lock() {
207         w.prepare_text(&mut fs);
208     }
209     let (style_r, corners) =
210         w.paint_model().corner_style(w.content_rect()).unwrap_or((0.0, (false, false, false, false)));
211     let r = if corners != (false, false, false, false) { style_r } else { 0.0 };
212     let (wx, mut wy, www, mut whh) = w.rect();
213     let top_room = w.label_strip();
214     wy += top_room;
215     whh -= top_room;
216 
217     // ONE ordered replay of the paint walk — the same walk the live display-
218     // list render runs (`scene::painter`), every prim in the order the widget
219     // painted it, each mapped onto the flat host's RenderTarget surface.
220     //
221     // This used to be three passes over three typed views of the same paint:
222     // the relief prims (downcast per widget type and re-derived from the
223     // widget's accessors — the Dropdown's inset plate, the TextBox's well, the
224     // Button's face, the Toggle's faces and steps), then every plain quad
225     // (`all_quads`), then every rounded quad (`all_rounded_quads`). Splitting
226     // one paint into typed streams loses the order between them, and the
227     // order is the picture: a TextBox draws its rounded background, carves
228     // its well, THEN lays the selection highlight and caret on top — the
229     // three-pass replay put the well under the highlight and the background
230     // over both. A Dropdown's hovered row is drawn after its menu plate; a
231     // host that replays plates after rects buries the highlight. The prim
232     // walk keeps the widget's order, covers every widget instead of the four
233     // that had a special case, and carries the relief prims' own per-corner
234     // radii and depth (the re-derivations rounded those off).
235     //
236     // Mapping onto the tuple surface: Quad and RoundedRect are the two
237     // native fills (the root's plain background keeps its solid-border
238     // expansion and window-corner resolution); a zero-stroke Border is an
239     // inset plate's FACE and is held until the Trough that follows it, so the
240     // pair reaches the host as ONE `inset_plate` call (a relief host carves
241     // it for real; the default degrades to the flat fill); Recess/Boss go to
242     // `relief_carve`; a Bevel degrades to its fill — what a flat host can
243     // draw of a raised plate. Ridges, circles, arcs, vectors and images have
244     // no flat-surface counterpart and are skipped, as they always were.
245     let solid_border = w.solid_border();
246     let mut text_scratch = crate::scene::paint::PaintCtx::new();
247     crate::scene::painter::paint_root_into(&*ctx, &*w, &mut text_scratch);
248 
249     // A zero-stroke Border waiting for its Trough: (rect, radii, fill).
250     let mut pending_face: Option<(crate::scene::layout::Rect, crate::scene::paint::Radii, [f32; 4])> = None;
251     fn same_rect(a: crate::scene::layout::Rect, b: crate::scene::layout::Rect) -> bool {
252         (a.x - b.x).abs() < 0.1 && (a.y - b.y).abs() < 0.1 && (a.width - b.width).abs() < 0.1 && (a.height - b.height).abs() < 0.1
253     }
254     fn emit_rounded(pc: &mut dyn RenderTarget, rect: crate::scene::layout::Rect, radii: crate::scene::paint::Radii, color: [f32; 4]) {
255         let (r1, r2, r3, r4) = radii;
256         let radius = r1.max(r2).max(r3).max(r4);
257         let mask = (r1 > 0.0, r2 > 0.0, r3 > 0.0, r4 > 0.0);
258         pc.rect_with_radius_corners(color, rect.x, rect.y, rect.width, rect.height, radius, mask);
259     }
260 
261     for item in text_scratch.finish().items {
262         use crate::scene::paint::Prim;
263         let trough_for_face = match (&item.prim, &pending_face) {
264             (Prim::Trough { rect, .. }, Some((face_rect, _, _))) => same_rect(*rect, *face_rect),
265             // `inset_plate`'s edge: a field that is all run.
266             (Prim::Field { rect, split, end, .. }, Some((face_rect, _, _))) => {
267                 *split <= rect.x && *end >= rect.x + rect.width && same_rect(*rect, *face_rect)
268             }
269             _ => false,
270         };
271         if !trough_for_face {
272             if let Some((frect, fradii, fill)) = pending_face.take() {
273                 emit_rounded(pc, frect, fradii, fill);
274             }
275         }
276         match item.prim {
277             Prim::Quad { rect, color: qc } => {
278                 let (qx, qy, qw, qh) = (rect.x, rect.y, rect.width, rect.height);
279                 let extra_corners = (
280                     corners.0 && qx <= wx + 1.5 && qy <= wy + 1.5,
281                     corners.1 && qx + qw >= wx + www - 1.5 && qy <= wy + 1.5,
282                     corners.2 && qx + qw >= wx + www - 1.5 && qy + qh >= wy + whh - 1.5,
283                     corners.3 && qx <= wx + 1.5 && qy + qh >= wy + whh - 1.5,
284                 );
285 
286                 let (resolved_r, resolved_corners) = if r <= 0.1 || corners == (false, false, false, false) || extra_corners == (false, false, false, false) {
287                     (0.0, (false, false, false, false))
288                 } else {
289                     (r, extra_corners)
290                 };
291 
292                 // The widget's own background quad with a solid border: full-size
293                 // border quad, then the inset background over it.
294                 let mut border_drawn = false;
295                 let is_bg_quad = (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - www).abs() < 0.1 && (qh - whh).abs() < 0.1;
296                 if is_bg_quad {
297                     if let Some((border_color, thickness)) = solid_border {
298                         if thickness > 0.0 {
299                             pc.rect_with_radius_corners(border_color, qx, qy, qw, qh, resolved_r, resolved_corners);
300                             pc.rect_with_radius_corners(
301                                 qc,
302                                 qx + thickness,
303                                 qy + thickness,
304                                 (qw - 2.0 * thickness).max(0.0),
305                                 (qh - 2.0 * thickness).max(0.0),
306                                 (resolved_r - thickness).max(0.0),
307                                 resolved_corners,
308                             );
309                             border_drawn = true;
310                         }
311                     }
312                 }
313 
314                 if !border_drawn {
315                     pc.rect_with_radius_corners(qc, qx, qy, qw, qh, resolved_r, resolved_corners);
316                 }
317             }
318             Prim::RoundedRect { rect, radius, corners: qcorners, color: qc } => {
319                 pc.rect_with_radius_corners(qc, rect.x, rect.y, rect.width, rect.height, radius, qcorners);
320             }
321             Prim::Border { rect, radii, fill, border, thickness } => {
322                 if thickness > 0.0 && border[3].abs() > 0.001 {
323                     emit_rounded(pc, rect, radii, border);
324                     if fill[3].abs() > 0.001 {
325                         let inner = crate::scene::layout::Rect {
326                             x: rect.x + thickness,
327                             y: rect.y + thickness,
328                             width: (rect.width - 2.0 * thickness).max(0.0),
329                             height: (rect.height - 2.0 * thickness).max(0.0),
330                         };
331                         let (r1, r2, r3, r4) = radii;
332                         let shrink = |v: f32| if v > 0.0 { (v - thickness).max(0.0) } else { 0.0 };
333                         emit_rounded(pc, inner, (shrink(r1), shrink(r2), shrink(r3), shrink(r4)), fill);
334                     }
335                 } else if fill[3].abs() > 0.001 {
336                     // abs(): a negative alpha is the frost sentinel, a real face.
337                     pending_face = Some((rect, radii, fill));
338                 }
339             }
340             // A flat host has no blended outline: the two-box form
341             // `Prim::Field` replaced — the well to the seam, the run's flush
342             // plate past it.
343             Prim::Field { rect, radii, depth, split, end, tint } => {
344                 let (fl, fr) = (rect.x, rect.x + rect.width);
345                 let (well_l, well_r) = (split > fl, end < fr);
346                 // All run and no well (`PaintCtx::inset_plate`'s edge): the
347                 // plate alone, with the face that came before it.
348                 let face = if !well_l && !well_r { pending_face.take().map(|(_, _, fill)| fill) } else { None }.unwrap_or([0.0; 4]);
349                 let rx = split.max(fl);
350                 let rw = (end.min(fr) - rx).max(0.0);
351                 let mut well = |x: f32, w: f32, radii: (f32, f32, f32, f32)| {
352                     pc.relief_carve(&ReliefCarve {
353                         kind: CarveKind::Recess { tint },
354                         x,
355                         y: rect.y,
356                         w,
357                         h: rect.height,
358                         radii,
359                         depth,
360                         edges: (true, true, true, true),
361                     });
362                 };
363                 if well_l {
364                     well(fl, rx - fl, (radii.0, 0.0, 0.0, radii.3));
365                 }
366                 if well_r {
367                     well(rx + rw, fr - rx - rw, (0.0, radii.1, radii.2, 0.0));
368                 }
369                 let r = if well_r { radii.0.max(radii.3) } else { radii.1.max(radii.2) };
370                 match tint {
371                     Some(t) => pc.inset_plate_tinted(face, rx, rect.y, rw, rect.height, r, depth, t),
372                     None => pc.inset_plate(face, rx, rect.y, rw, rect.height, r, depth),
373                 }
374             }
375             Prim::Trough { rect, radii, depth, tint, .. } => {
376                 let face = pending_face.take().map(|(_, _, fill)| fill).unwrap_or([0.0; 4]);
377                 let (r1, r2, r3, r4) = radii;
378                 let r = r1.max(r2).max(r3).max(r4);
379                 match tint {
380                     Some(t) => pc.inset_plate_tinted(face, rect.x, rect.y, rect.width, rect.height, r, depth, t),
381                     None => pc.inset_plate(face, rect.x, rect.y, rect.width, rect.height, r, depth),
382                 }
383             }
384             Prim::Bevel { rect, radii, material, .. } => {
385                 let color = material.fill(crate::scene::material::PlateRole::Nested);
386                 if color[3].abs() > 0.001 {
387                     emit_rounded(pc, rect, radii, color);
388                 }
389             }
390             Prim::Recess { rect, radii, depth, edges, tint } => {
391                 pc.relief_carve(&ReliefCarve {
392                     kind: CarveKind::Recess { tint },
393                     x: rect.x,
394                     y: rect.y,
395                     w: rect.width,
396                     h: rect.height,
397                     radii,
398                     depth,
399                     edges,
400                 });
401             }
402             Prim::Boss { rect, radii, depth, edges, tint } => {
403                 pc.relief_carve(&ReliefCarve {
404                     kind: CarveKind::Boss { tint },
405                     x: rect.x,
406                     y: rect.y,
407                     w: rect.width,
408                     h: rect.height,
409                     radii,
410                     depth,
411                     edges,
412                 });
413             }
414             // Text via the paint walk: each widget's Text prims (content font + scroll-ancestor
415             // clip) exactly as the live display-list render does; the prim already carries the
416             // per-widget font + bounds.
417             Prim::Text { text, x, y, font_size, color, font, bounds, .. } => {
418                 let color_f32 = [
419                     color[0] as f32 / 255.0,
420                     color[1] as f32 / 255.0,
421                     color[2] as f32 / 255.0,
422                     1.0,
423                 ];
424                 // Compose the walk's container clip with the prim's own bounds (the engine's dl-text
425                 // merge), so a clipping ancestor still bounds the text.
426                 let clip = item.clip.map(|c| [c.x, c.y, c.x + c.width, c.y + c.height]);
427                 let merged = match (clip, bounds) {
428                     (Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
429                     (Some(a), None) => Some(a),
430                     (None, b) => b,
431                 };
432                 match font {
433                     Some(ref f) => pc.text_with_font_and_bounds(&text, x, y, font_size, color_f32, f, merged),
434                     None => pc.text_with_bounds(&text, x, y, font_size, color_f32, merged),
435                 }
436             }
437             // Strokes, arcs and discs — a graph's wires and ports, chiefly.
438             // Until 2026-10-06 these were dropped too, so a graph in a flat
439             // host (cce-files' Graph page) drew its nodes and no wires.
440             Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => {
441                 if let Some(c) = item.clip {
442                     pc.push_clip_rect(c.x, c.y, c.width, c.height);
443                 }
444                 pc.line(x1, y1, x2, y2, thickness, color, cap);
445                 if item.clip.is_some() {
446                     pc.pop_clip_rect();
447                 }
448             }
449             Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
450                 if let Some(c) = item.clip {
451                     pc.push_clip_rect(c.x, c.y, c.width, c.height);
452                 }
453                 pc.arc(cx, cy, radius, thickness, start, end, color);
454                 if item.clip.is_some() {
455                     pc.pop_clip_rect();
456                 }
457             }
458             Prim::Circle { cx, cy, radius, color } => {
459                 if let Some(c) = item.clip {
460                     pc.push_clip_rect(c.x, c.y, c.width, c.height);
461                 }
462                 pc.circle(cx, cy, radius, color);
463                 if item.clip.is_some() {
464                     pc.pop_clip_rect();
465                 }
466             }
467             // A widget's GLYPH (a dropdown's arrow, a spinbox's −/+, a menu
468             // mark): drawn by name through the host's `RenderTarget::icon`,
469             // since an image id means nothing to a flat host. Until this arm
470             // every widget glyph vanished from a flat host — the toolkit drew
471             // its symbols as text before 2026-10-05, and this replay dropped
472             // images. An image that is not a bundled glyph is still dropped.
473             Prim::Image { image, rect, alpha } => {
474                 if let Some((name, _, tint)) = crate::icon_source(image) {
475                     let [r, g, b] = tint.unwrap_or([255, 255, 255]);
476                     let color = [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, alpha];
477                     if let Some(c) = item.clip {
478                         pc.push_clip_rect(c.x, c.y, c.width, c.height);
479                     }
480                     pc.icon(&name, rect, color);
481                     if item.clip.is_some() {
482                         pc.pop_clip_rect();
483                     }
484                 }
485             }
486             _ => {}
487         }
488     }
489     if let Some((frect, fradii, fill)) = pending_face.take() {
490         emit_rounded(pc, frect, fradii, fill);
491     }
492     if w.popover_rect().is_some() {
493         ctx.register_popover_id(w.base().id());
494     }
495 }
496 
497 pub fn render_popovers(pc: &mut dyn RenderTarget, ctx: &UiContext) {
498     for &pop_id in &ctx.active_popovers {
499         if let Some(ptr) = ctx.tree.get_ptr(pop_id) {
500             unsafe {
501                 (*ptr).render_popover(pc);
502             }
503         }
504     }
505     // Registry sweep for open popovers the app never registered (popover
506     // registration is optional and spotty) — the same fallback the coverage
507     // check and the engine's outside-press close use.
508     for (id, ptr) in ctx.tree.iter_registered() {
509         if ctx.active_popovers.contains(&id) {
510             continue;
511         }
512         unsafe {
513             if let Some(w) = ptr.as_ref() {
514                 if w.visible() && w.popover_rect().is_some() {
515                     w.render_popover(pc);
516                 }
517             }
518         }
519     }
520 }
521 
522 pub fn partition_concentric_corners(
523     x: f32, y: f32, w: f32, h: f32,
524     _r_std: f32,
525     r_adjust: [f32; 4],
526     color: [f32; 4],
527 ) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
528     let mut quads = Vec::new();
529 
530     let r0 = r_adjust[0];
531     let r1 = r_adjust[1];
532     let r2 = r_adjust[2];
533     let r3 = r_adjust[3];
534 
535     let max_left = r0.max(r3);
536     let max_right = r1.max(r2);
537 
538     let mut push_valid_quad = |qx: f32, qy: f32, qw: f32, qh: f32, qr: f32, qcorners: (bool, bool, bool, bool)| {
539         if qw > 0.001 && qh > 0.001 {
540             quads.push((qx, qy, qw, qh, qr, color, qcorners));
541         }
542     };
543 
544     // 1. Center vertical block
545     push_valid_quad(x + max_left, y, w - max_left - max_right, h, 0.0, (false, false, false, false));
546 
547     // 2. Left block
548     push_valid_quad(x, y + r0, max_left, h - r0 - r3, 0.0, (false, false, false, false));
549 
550     // 3. Top-left transition
551     push_valid_quad(x + r0, y, max_left - r0, r0, 0.0, (false, false, false, false));
552 
553     // 4. Bottom-left transition
554     push_valid_quad(x + r3, y + h - r3, max_left - r3, r3, 0.0, (false, false, false, false));
555 
556     // 5. Right block
557     push_valid_quad(x + w - max_right, y + r1, max_right, h - r1 - r2, 0.0, (false, false, false, false));
558 
559     // 6. Top-right transition
560     push_valid_quad(x + w - max_right, y, max_right - r1, r1, 0.0, (false, false, false, false));
561 
562     // 7. Bottom-right transition
563     push_valid_quad(x + w - max_right, y + h - r2, max_right - r2, r2, 0.0, (false, false, false, false));
564 
565     // 8. Corner 0 (top-left)
566     push_valid_quad(x, y, r0, r0, r0, (true, false, false, false));
567 
568     // 9. Corner 1 (top-right)
569     push_valid_quad(x + w - r1, y, r1, r1, r1, (false, true, false, false));
570 
571     // 10. Corner 2 (bottom-right)
572     push_valid_quad(x + w - r2, y + h - r2, r2, r2, r2, (false, false, true, false));
573 
574     // 11. Corner 3 (bottom-left)
575     push_valid_quad(x, y + h - r3, r3, r3, r3, (false, false, false, true));
576 
577     quads
578 }