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 }