git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commit05540be57a2a261bca4dadd3c48f00bfde454dff
parentc12081b47f
authorLucas Galante <[email protected]>
date2026-07-12 09:34
refactor: JsonLayoutWidget onto the narrow traits (Phase 6az part 4) — cloud is raw-impl-free

The launcher-layout host converts to Layout/Paint/Input in Adapted, the
TreeList shape: paints_own_subtree, whole-subtree event routing in on_event
(gates_presses off — the trailing focus-clear needs every press, like the
ungated legacy direct dispatch), children ticked via tick_ctx. The old
Element overrides survive verbatim as inherent aggregate_quads/tick_children/
route_event; paint composes them with a dummy UiContext (the Adapted children
read nothing from ctx in these paths) and replicates the deleted Element
default's unfiltered child rounded-quad recursion. Render sites keep calling
jl.all_quads/all_rounded_quads — they now resolve to the adapter's reverse
bridges serving the same tuples. Constructor returns the wrapper; field reads
ride Deref. Also fixes a test-only import stale since 6ay
(cce_ui::widget::JsonWidgetConfig -> crate::json_layout::).

Verified: 8/8 tests (incl. the widget-flow layout test through the wrapper);
JSON-mode A/B overlay pixel-identical (all >=8% diffs are the terminal behind
the translucent plate); live checkbox click toggles checked state off.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk

 src/json_layout.rs | 159 +++++++++++++++++++++++++++--------------------------
 src/main.rs        |   4 +-
 2 files changed, 84 insertions(+), 79 deletions(-)

diff --git a/src/json_layout.rs b/src/json_layout.rs
index 8fe698e..935ebd2 100644
--- a/src/json_layout.rs
+++ b/src/json_layout.rs
@@ -1,8 +1,8 @@
 //! App-owned copy of the dissolved cce-ui `JsonLayoutWidget` (Phase 6ay): cloud is the
-//! only consumer — the KDL/JSON-driven launcher layout host (`LauncherMode::Json`). It
-//! still implements `Element` because the paint walk (`append_widget_text`) descends it;
-//! the impl dies with the machinery retype. `Justification` stayed in cce-ui (Button,
-//! cce-files, and settings share it).
+//! only consumer — the KDL/JSON-driven launcher layout host (`LauncherMode::Json`),
+//! on the narrow traits wrapped in `Adapted<JsonLayoutWidget>` (Phase 6az): the paint
+//! walk reaches it as the adapter, whose subtree text pass-through forwards `paint`'s
+//! prims verbatim. `Justification` stayed in cce-ui (Button, cce-files, settings).
 
 use cce_ui::widget::{
     Element, Widget, Checkbox, Button, Label, Spinbox, ColorSelector, TextLabel, MouseButton, ElementState, Slider, Event, UiContext,
@@ -69,7 +69,7 @@ pub struct JsonLayoutWidget {
 }
 
 impl JsonLayoutWidget {
-    pub fn new(config: &JsonLayoutConfig) -> Self {
+    pub fn new(config: &JsonLayoutConfig) -> cce_ui::widget::Adapted<JsonLayoutWidget> {
         let mut widgets = Vec::new();
         let mut page_titles = Vec::new();
 
@@ -220,14 +220,14 @@ impl JsonLayoutWidget {
             }
         }
 
-        Self {
+        cce_ui::widget::Adapted::new(Self {
             base: Widget::new(),
             widgets,
             dragging_slider_idx: None,
             page_scroll_y: vec![0.0; 16],
             page_total_heights: vec![0.0; 16],
             active_page: 0,
-        }
+        })
     }
 
     pub fn layout_children(&mut self) {
@@ -290,50 +290,17 @@ impl JsonLayoutWidget {
     }
 }
 
-impl Element for JsonLayoutWidget {
-    fn is_scrollable(&self) -> bool { true }
-    fn as_any(&self) -> &dyn std::any::Any { self }
-    fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
-    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
-        self as *const Self as *mut Self as *mut (dyn Element + 'static)
-    }
-    fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
-        self as *mut Self as *mut (dyn Element + 'static)
-    }
-
-    fn base(&self) -> Option<&Widget> { Some(&self.base) }
-    fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
-    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        if let Some(b) = self.base_mut() {
-            b.x = x;
-            b.y = y;
-            b.w = w;
-            b.h = h;
-        }
-        self.layout_children();
-    }
-
-    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
-        let mut changed = false;
-        let active_page = self.active_page;
-        for w in &mut self.widgets {
-            if w.page_idx != active_page {
-                continue;
-            }
-            if w.widget.tick(dt, ctx) {
-                changed = true;
-            }
-        }
-        changed
-    }
-
-    fn wants_tick(&self) -> bool {
-        true
+impl JsonLayoutWidget {
+    /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned`.
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        (self.base.x, self.base.y, self.base.w, self.base.h)
     }
 
-    fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+    /// The page-filtered plain-quad aggregate (the old `Element::all_quads` override):
+    /// active-page children's backgrounds, decoration quads, and highlight, clipped to
+    /// the content area. External readers reach it through the adapter's reverse bridge
+    /// (`jl.all_quads(ctx)` serves `paint`'s plain prims, which come from here).
+    fn aggregate_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let mut quads = Vec::new();
 
         let active_page = self.active_page;
@@ -378,7 +345,26 @@ impl Element for JsonLayoutWidget {
         quads
     }
 
-    fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
+    /// Per-frame child state (the old `Element::tick` override): active-page widgets only.
+    fn tick_children(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
+        let mut changed = false;
+        let active_page = self.active_page;
+        for w in &mut self.widgets {
+            if w.page_idx != active_page {
+                continue;
+            }
+            if w.widget.tick(dt, ctx) {
+                changed = true;
+            }
+        }
+        changed
+    }
+
+    /// The whole-subtree event routing (the old `Element::handle_event` override,
+    /// verbatim). Every press reaches it (`Input::gates_presses` is off), matching the
+    /// ungated legacy direct-dispatch path — the trailing focus-clear on a missed press
+    /// depends on that.
+    fn route_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
         let mut changed = false;
 
         match event {
@@ -553,47 +539,66 @@ impl Element for JsonLayoutWidget {
 
         changed
     }
+}
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py }, ctx)
-    }
-
-    fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.handle_event(&Event::PointerMove { x: px, y: py, local_x: px, local_y: py }, ctx)
+impl cce_ui::widget::Layout for JsonLayoutWidget {
+    // The old `set_rect` override: land the rect in the model, then place the children.
+    fn rect_assigned(&mut self, rect: cce_ui::scene::layout::Rect) {
+        self.base.x = rect.x;
+        self.base.y = rect.y;
+        self.base.w = rect.width;
+        self.base.h = rect.height;
+        self.layout_children();
     }
+}
 
-    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
-        self.widgets.iter().map(|w| w.widget.as_ptr()).collect()
-    }
+impl cce_ui::widget::Paint for JsonLayoutWidget {
+    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
 
-    // JsonLayout renders its whole subtree itself: page-filtered aggregates plus the
-    // checkbox side-labels that belong to the container, not to any child widget. The
-    // paint walk must emit these once and not descend (descending would draw inactive
-    // pages' widgets and miss the side-labels).
-    fn renders_own_subtree(&self) -> bool {
-        true
-    }
+    // JsonLayout paints its whole subtree: page-filtered aggregates plus the checkbox
+    // side-labels that belong to the container, not to any child widget. The paint walk
+    // emits these once and does not descend (descending would draw inactive pages and
+    // miss the side-labels); the adapter forwards the Text prims verbatim, bounds included.
+    fn paints_own_subtree(&self) -> bool { true }
 
-    fn paint_self(&self, ui: &UiContext, pc: &mut cce_ui::scene::paint::PaintCtx) {
+    fn paint(&self, _rect: cce_ui::scene::layout::Rect, pc: &mut cce_ui::scene::paint::PaintCtx) {
         use cce_ui::scene::layout::Rect;
-        for (x, y, w, h, r, c, corners) in self.all_rounded_quads(ui) {
-            pc.rounded_rect(Rect { x, y, width: w, height: h }, r, corners, c);
+        // The children are Phase 5 Adapted leaves: nothing their all_* getters or the
+        // label walk reads comes from the routing context, so a fresh one stands in for
+        // the ctx `Paint::paint` does not carry.
+        let dummy = UiContext::new();
+        // Rounded: the deleted Element default's shape — no own background (transparent,
+        // sharp corners), every child unfiltered, in `widgets` order.
+        for w in &self.widgets {
+            for (x, y, qw, qh, r, c, corners) in w.widget.all_rounded_quads(&dummy) {
+                pc.rounded_rect(Rect { x, y, width: qw, height: qh }, r, corners, c);
+            }
         }
-        for (x, y, w, h, c) in self.all_quads(ui) {
+        for (x, y, w, h, c) in self.aggregate_quads(&dummy) {
             pc.quad(Rect { x, y, width: w, height: h }, c);
         }
-        for (cx, cy, r, t, s, e, c) in self.extra_arcs() {
-            pc.arc(cx, cy, r, t, s, e, c);
-        }
-        for (cx, cy, r, c) in self.extra_circles() {
-            pc.circle(cx, cy, r, c);
-        }
-        for (tl, bounds) in self.own_labels_with_bounds(ui) {
+        for (tl, bounds) in self.own_labels_with_bounds(&dummy) {
             pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, None, bounds);
         }
     }
 }
 
+impl cce_ui::widget::Input for JsonLayoutWidget {
+    fn scrollable(&self) -> bool { true }
+    fn wants_tick(&self) -> bool { true }
+    fn gates_presses(&self) -> bool { false }
+
+    fn on_event(&mut self, event: &Event, ectx: &mut cce_ui::widget::EventCtx) -> bool {
+        let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
+        self.route_event(event, ctx)
+    }
+
+    fn tick_ctx(&mut self, dt: f32, ectx: &mut cce_ui::widget::EventCtx) -> bool {
+        let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
+        self.tick_children(dt, ctx)
+    }
+}
+
 impl JsonLayoutWidget {
     pub(crate) fn own_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
         let mut labels = Vec::new();
diff --git a/src/main.rs b/src/main.rs
index 31fc8e1..0ab7945 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,7 +8,7 @@ use cce_ui::widget::{Element, TextLabel};
 use crate::json_layout::{JsonLayoutWidget, JsonLayoutConfig};
 mod json_layout;
 #[cfg(test)]
-use cce_ui::widget::JsonWidgetConfig;
+use crate::json_layout::JsonWidgetConfig;
 
 use smithay_client_toolkit::{
     compositor::{CompositorHandler, CompositorState},
@@ -698,7 +698,7 @@ struct State {
     vertex_count: u32,
 
     fuzzel: cce_ui::widget::Adapted<FuzzelWidget>,
-    json_layout: Option<JsonLayoutWidget>,
+    json_layout: Option<cce_ui::widget::Adapted<JsonLayoutWidget>>,
     font_system: FontSystem,
     swash_cache: SwashCache,
     text_atlas: TextAtlas,