GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(widget): make SplitBox aggregate its children like a real container
SplitBox only forwarded rounded quads (via the default recursive
all_rounded_quads); it used the default all_quads / text_labels*, which
do not recurse into children. Any self-rendering widget placed inside a
split (e.g. cce-files' PreviewState) therefore drew nothing.
Aggregate each child's quads and text, clipped to the split's bounds,
mirroring Backplate:
- all_quads: divider handles + children, skipping a child's own rounded
background so it is not doubled by a square underlay.
- all_rounded_quads: children's rounded quads, now clipped to bounds
(fixes overflow into the sibling when the divider is dragged small).
- text_labels{,_with_bounds,_with_font_and_bounds} and get_text_items:
forwarded and bound-clipped.
A widget emits its background through exactly one path (rounded ->
all_rounded_quads, otherwise extra_quads/all_quads), so this cannot
double-render backgrounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
src/widget/container/splitbox.rs | 126 +++++++++++++++++++++++++++++++++++++++
1 file changed, 126 insertions(+)
diff --git a/src/widget/container/splitbox.rs b/src/widget/container/splitbox.rs
index 4046903..79e128e 100644
--- a/src/widget/container/splitbox.rs
+++ b/src/widget/container/splitbox.rs
@@ -299,6 +299,132 @@ impl Element for SplitBox {
quads
}
+ // SplitBox is a real layout container: aggregate each child's quads and text
+ // (clipped to our bounds), so a self-rendering widget placed inside the split
+ // is drawn through the widget tree rather than needing to be rendered manually.
+ // Mirrors Backplate's aggregation. A child's own background is emitted through
+ // exactly one path (rounded → all_rounded_quads, otherwise extra_quads), so the
+ // has_rounded guard below prevents a square underlay from doubling a rounded bg.
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible() {
+ return Vec::new();
+ }
+ let mut quads = self.extra_quads();
+ let (wx, wy, ww, wh) = self.rect();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ let (cx, cy, cw, ch) = widget.rect();
+ let child_has_rounded = widget.rounded_corners() != (false, false, false, false);
+ for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
+ if child_has_rounded
+ && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1
+ && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1
+ {
+ continue;
+ }
+ let x0 = qx.max(wx);
+ let y0 = qy.max(wy);
+ let x1 = (qx + qw).min(wx + ww);
+ let y1 = (qy + qh).min(wy + wh);
+ if x1 > x0 && y1 > y0 {
+ quads.push((x0, y0, x1 - x0, y1 - y0, qc));
+ }
+ }
+ }
+ quads
+ }
+
+ fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+ if !self.visible() {
+ return Vec::new();
+ }
+ let mut quads = Vec::new();
+ let (wx, wy, ww, wh) = self.rect();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ for (qx, qy, qw, qh, qr, qc, corners) in widget.all_rounded_quads(ctx) {
+ let x0 = qx.max(wx);
+ let y0 = qy.max(wy);
+ let x1 = (qx + qw).min(wx + ww);
+ let y1 = (qy + qh).min(wy + wh);
+ if x1 > x0 && y1 > y0 {
+ quads.push((x0, y0, x1 - x0, y1 - y0, qr, qc, corners));
+ }
+ }
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ labels.extend(widget.text_labels());
+ }
+ labels
+ }
+
+ fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ let mut result = Vec::new();
+ let (wx, wy, ww, wh) = self.rect();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ for (label, bounds) in widget.text_labels_with_bounds(ctx) {
+ let cb = match bounds {
+ Some(b) => {
+ let cx0 = b[0].max(wx);
+ let cy0 = b[1].max(wy);
+ let cx1 = b[2].min(wx + ww);
+ let cy1 = b[3].min(wy + wh);
+ if cx1 > cx0 && cy1 > cy0 {
+ Some([cx0, cy0, cx1, cy1])
+ } else {
+ continue;
+ }
+ }
+ None => Some([wx, wy, wx + ww, wy + wh]),
+ };
+ result.push((label, cb));
+ }
+ }
+ result
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ let mut result = Vec::new();
+ let (wx, wy, ww, wh) = self.rect();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ for (label, font, bounds) in widget.text_labels_with_font_and_bounds(ctx) {
+ let cb = match bounds {
+ Some(b) => {
+ let cx0 = b[0].max(wx);
+ let cy0 = b[1].max(wy);
+ let cx1 = b[2].min(wx + ww);
+ let cy1 = b[3].min(wy + wh);
+ if cx1 > cx0 && cy1 > cy0 {
+ Some([cx0, cy0, cx1, cy1])
+ } else {
+ continue;
+ }
+ }
+ None => Some([wx, wy, wx + ww, wy + wh]),
+ };
+ result.push((label, font, cb));
+ }
+ }
+ result
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ let mut result = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ result.extend(widget.get_text_items());
+ }
+ result
+ }
+
fn set_hovered(&mut self, v: bool) {
if !v {
self.hovered_idx = None;