git.lucas.co / cce-color-editor
color picker and palette editor
git clone https://git.lucas.co/cce-color-editor.git

commit3accc1888df3688f51c0dfb800eb113c0c2a0255
parent49493c9dcf
authorLucas Galante <[email protected]>
date2026-06-19 02:56
Refactor color interface to use cce-ui window as base object and runner engine

 Cargo.toml  |    9 +-
 src/main.rs | 2062 ++++++++++++++++++++++-------------------------------------
 2 files changed, 758 insertions(+), 1313 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index bd4f6bf..a925abb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,13 +5,10 @@ edition = "2021"
 
 [dependencies]
 cce-ui = { path = "../cce-ui" }
-smithay-client-toolkit = "0.19.2"
-calloop = "0.13.0"
-calloop-wayland-source = "0.3.0"
 wayland-client = { version = "0.31", features = ["system"] }
-xkeysym = "0.2"
-raw-window-handle = "0.6"
 wgpu = "24"
 bytemuck = { version = "1", features = ["derive"] }
-pollster = "0.4"
 glyphon = "0.8"
+calloop = "0.13.0"
+tokio = { version = "1", features = ["full"] }
+
diff --git a/src/main.rs b/src/main.rs
index 1402d0f..2a12ee7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,226 +1,339 @@
-use glyphon::{
-    Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
-    TextBounds, TextRenderer, Viewport,
+use cce_ui::engine::{Application, WindowSettings, LogicalSize, LogicalPosition, EngineState};
+use cce_ui::widget::{
+    Window, Button, Element, UiContext, MouseButton, ElementState, KeyEvent, MouseScrollDelta,
+    Widget, display::TextLabel,
 };
-use cce_ui::color;
-
-use smithay_client_toolkit::{
-    compositor::{CompositorHandler, CompositorState},
-    delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
-    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
-    registry::{ProvidesRegistryState, RegistryState},
-    output::{OutputHandler, OutputState},
-    seat::{
-        keyboard::KeyboardHandler,
-        pointer::PointerHandler,
-        Capability, SeatHandler, SeatState,
-    },
-    shell::{
-        xdg::{
-            window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
-            XdgShell,
-        },
-        WaylandSurface,
-    },
-    shm::{Shm, ShmHandler},
-};
-use wayland_client::{
-    globals::registry_queue_init,
-    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
-    Connection, QueueHandle, Proxy,
-};
-use calloop::EventLoop;
-use calloop_wayland_source::WaylandSource;
+use cce_ui::widget::focus::link_parent_child;
+use cce_ui::layout::RenderTarget;
+use glyphon::FontSystem;
+use wayland_client::QueueHandle;
 
-#[repr(C)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct Vertex {
-    position: [f32; 2],
-    color: [f32; 4],
-    clip_circle: [f32; 3],
+
+const HEADER_H: f32 = 36.0;
+const SLIDER_ROW_H: f32 = 36.0;
+const SLIDER_START_Y: f32 = 48.0;
+const SLIDER_LABEL_X: f32 = 12.0;
+const SLIDER_TRACK_X: f32 = 32.0;
+const SLIDER_TRACK_W: f32 = 280.0;
+const SLIDER_TRACK_H: f32 = 20.0;
+const SLIDER_VALUE_X: f32 = 320.0;
+const PREVIEW_X: f32 = 12.0;
+const PREVIEW_Y: f32 = 276.0;
+const PREVIEW_W: f32 = 160.0;
+const PREVIEW_H: f32 = 72.0;
+const BUTTON_Y: f32 = 360.0;
+const BUTTON_H: f32 = 32.0;
+const BUTTON_W: f32 = 100.0;
+const BUTTON_GAP: f32 = 12.0;
+
+// ── PageContent for custom Target Rendering ────────────────────────
+
+pub struct PageContent {
+    pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
+    pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
 }
 
-impl Vertex {
-    const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
-        0 => Float32x2,
-        1 => Float32x4,
-        2 => Float32x3,
-    ];
-
-    fn desc() -> wgpu::VertexBufferLayout<'static> {
-        wgpu::VertexBufferLayout {
-            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
-            step_mode: wgpu::VertexStepMode::Vertex,
-            attributes: &Self::ATTRIBS,
+impl PageContent {
+    pub fn new() -> Self {
+        Self {
+            rects: Vec::new(),
+            texts: Vec::new(),
         }
     }
 }
 
-fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
-    let x0 = (x / sw) * 2.0 - 1.0;
-    let y0 = 1.0 - (y / sh) * 2.0;
-    let x1 = ((x + w) / sw) * 2.0 - 1.0;
-    let y1 = 1.0 - ((y + h) / sh) * 2.0;
-    [
-        Vertex { position: [x0, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x1, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
-    ]
-}
+impl cce_ui::layout::RenderTarget for PageContent {
+    fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
+        self.rects.push((color, x, y, w, h, 0.0, (true, true, true, true)));
+    }
+
+    fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
+        self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
+    }
 
-fn gradient_quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c0: [f32; 4], c1: [f32; 4]) -> [Vertex; 6] {
-    let x0 = (x / sw) * 2.0 - 1.0;
-    let y0 = 1.0 - (y / sh) * 2.0;
-    let x1 = ((x + w) / sw) * 2.0 - 1.0;
-    let y1 = 1.0 - ((y + h) / sh) * 2.0;
-    [
-        Vertex { position: [x0, y0], color: c0, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x1, y0], color: c1, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x0, y1], color: c0, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x1, y0], color: c1, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x1, y1], color: c1, clip_circle: [0.0, 0.0, 0.0] },
-        Vertex { position: [x0, y1], color: c0, clip_circle: [0.0, 0.0, 0.0] },
-    ]
+    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)) {
+        self.rects.push((color, x, y, w, h, radius, corners));
+    }
+
+    fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
+        self.texts.push((content.to_string(), size, x, y, color, None, None));
+    }
+
+    fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
+    }
+
+    fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
+        self.texts.push((content.to_string(), size, x, y, color, None, bounds));
+    }
+
+    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]>) {
+        self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
+    }
 }
 
-fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
-    let metrics = Metrics::new(size, size * 1.4);
-    let mut buf = Buffer::new(fs, metrics);
-    buf.set_text(fs, text, Attrs::new(), glyphon::Shaping::Advanced);
-    buf.shape_until_scroll(fs, true);
-    buf
+// ── Custom ColorSlider Widget ────────────────────────────────────────
+
+#[derive(Debug, Clone)]
+struct ColorSlider {
+    base: Widget,
+    value: f32,
+    channel_index: usize,
+    dragging: bool,
+    label: String,
+    just_changed: bool,
+
+    r: f32, g: f32, b: f32,
+    h: f32, s: f32, l: f32,
+    a: f32,
 }
 
-fn parse_hex(hex: &str) -> Option<(f32, f32, f32, Option<f32>)> {
-    let s = hex.trim_start_matches('#');
-    if s.len() == 6 {
-        u32::from_str_radix(s, 16).ok().map(|v| {
-            let r = ((v >> 16) & 0xFF) as f32 / 255.0;
-            let g = ((v >> 8) & 0xFF) as f32 / 255.0;
-            let b = (v & 0xFF) as f32 / 255.0;
-            (r, g, b, None)
-        })
-    } else if s.len() == 8 {
-        u32::from_str_radix(s, 16).ok().map(|v| {
-            let r = ((v >> 24) & 0xFF) as f32 / 255.0;
-            let g = ((v >> 16) & 0xFF) as f32 / 255.0;
-            let b = ((v >> 8) & 0xFF) as f32 / 255.0;
-            let a = (v & 0xFF) as f32 / 255.0;
-            (r, g, b, Some(a))
-        })
-    } else {
-        None
+impl ColorSlider {
+    pub fn new(label: &str, channel_index: usize) -> Self {
+        Self {
+            base: Widget::new_rect(0.0, 0.0, 0.0, 0.0),
+            value: 0.5,
+            channel_index,
+            dragging: false,
+            label: label.to_string(),
+            just_changed: false,
+            r: 0.5, g: 0.5, b: 0.5,
+            h: 0.0, s: 0.0, l: 0.5,
+            a: 1.0,
+        }
     }
 }
 
-fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
-    let max = r.max(g.max(b));
-    let min = r.min(g.min(b));
-    let mut h = 0.0;
-    let mut s = 0.0;
-    let l = (max + min) / 2.0;
+impl Element for ColorSlider {
+    cce_ui::impl_widget_base!(ColorSlider);
 
-    if max != min {
-        let d = max - min;
-        s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
-        if max == r {
-            h = (g - b) / d + (if g < b { 6.0 } else { 0.0 });
-        } else if max == g {
-            h = (b - r) / d + 2.0;
-        } else if max == b {
-            h = (r - g) / d + 4.0;
-        }
-        h /= 6.0;
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
     }
 
-    (h, s, l)
-}
+    fn preferred_height(&self) -> Option<f32> {
+        Some(SLIDER_ROW_H)
+    }
 
-fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
-    if s == 0.0 {
-        return (l, l, l);
+    fn draggable(&self) -> bool {
+        true
     }
 
-    let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
-    let p = 2.0 * l - q;
+    fn is_dragging(&self) -> bool {
+        self.dragging
+    }
 
-    let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
-    let g = hue_to_rgb(p, q, h);
-    let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
+    fn drag_begin(&mut self, _px: f32, _py: f32) {
+        self.dragging = true;
+    }
 
-    (r, g, b)
-}
+    fn drag_end(&mut self) {
+        self.dragging = false;
+    }
 
-fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
-    if t < 0.0 { t += 1.0; }
-    if t > 1.0 { t -= 1.0; }
-    if t < 1.0 / 6.0 { return p + (q - p) * 6.0 * t; }
-    if t < 1.0 / 2.0 { return q; }
-    if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
-    p
-}
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
+        if button != MouseButton::Left {
+            return false;
+        }
+        let (x, y, _, h) = self.rect();
+        let track_x = x + SLIDER_TRACK_X;
+        let track_w = SLIDER_TRACK_W;
+        let track_h = SLIDER_TRACK_H;
+        let track_y = y + (h - track_h) / 2.0;
+
+        if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
+            if state == ElementState::Pressed {
+                self.dragging = true;
+                let val = ((px - track_x) / track_w).clamp(0.0, 1.0);
+                self.value = val;
+                self.just_changed = true;
+                return true;
+            } else {
+                self.dragging = false;
+                return true;
+            }
+        }
+        false
+    }
+
+    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+        let (x, _, _, _) = self.rect();
+        let track_x = x + SLIDER_TRACK_X;
+        let track_w = SLIDER_TRACK_W;
+        let val = ((px - track_x) / track_w).clamp(0.0, 1.0);
+        if (val - self.value).abs() > 0.001 {
+            self.value = val;
+            self.just_changed = true;
+            return true;
+        }
+        false
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        let (x, y, _, h) = self.rect();
+
+        let track_x = x + SLIDER_TRACK_X;
+        let track_w = SLIDER_TRACK_W;
+        let track_h = SLIDER_TRACK_H;
+        let track_y = y + (h - track_h) / 2.0;
+
+        // 1. Draw track border
+        quads.push((
+            track_x - 1.0,
+            track_y - 1.0,
+            track_w + 2.0,
+            track_h + 2.0,
+            cce_ui::color::color_borders_color(),
+        ));
+
+        // 2. Draw checkerboard behind alpha track (channel_index == 6)
+        if self.channel_index == 6 {
+            // base background
+            quads.push((track_x, track_y, track_w, track_h, [0.8, 0.8, 0.8, 1.0]));
+
+            let grid_size = track_h / 2.0;
+            let cols = (track_w / grid_size).ceil() as i32;
+            for r in 0..2 {
+                for c in 0..cols {
+                    if (r + c) % 2 == 1 {
+                        let qx = track_x + c as f32 * grid_size;
+                        let qy = track_y + r as f32 * grid_size;
+                        let qw = grid_size.min(track_x + track_w - qx);
+                        let qh = grid_size.min(track_y + track_h - qy);
+                        if qw > 0.0 && qh > 0.0 {
+                            quads.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
+                        }
+                    }
+                }
+            }
+        }
 
+        // 3. Draw gradient track segments
+        let n_segments = if self.channel_index == 3 { 30 } else { 10 };
+        let get_color_at = |t: f32| -> [f32; 4] {
+            match self.channel_index {
+                0 => [t, self.g, self.b, self.a],
+                1 => [self.r, t, self.b, self.a],
+                2 => [self.r, self.g, t, self.a],
+                3 => {
+                    let (r, g, b) = hsl_to_rgb(t, self.s, self.l);
+                    [r, g, b, self.a]
+                }
+                4 => {
+                    let (r, g, b) = hsl_to_rgb(self.h, t, self.l);
+                    [r, g, b, self.a]
+                }
+                5 => {
+                    let (r, g, b) = hsl_to_rgb(self.h, self.s, t);
+                    [r, g, b, self.a]
+                }
+                _ => [self.r, self.g, self.b, t],
+            }
+        };
 
-const HEADER_H: f32 = 36.0;
-const SLIDER_ROW_H: f32 = 36.0;
-const SLIDER_START_Y: f32 = 48.0;
-const SLIDER_LABEL_X: f32 = 12.0;
-const SLIDER_TRACK_X: f32 = 32.0;
-const SLIDER_TRACK_W: f32 = 280.0;
-const SLIDER_TRACK_H: f32 = 20.0;
-const SLIDER_VALUE_X: f32 = 320.0;
-const PREVIEW_X: f32 = 12.0;
-const PREVIEW_Y: f32 = 276.0;
-const PREVIEW_W: f32 = 160.0;
-const PREVIEW_H: f32 = 72.0;
-const BUTTON_Y: f32 = 360.0;
-const BUTTON_H: f32 = 32.0;
-const BUTTON_W: f32 = 100.0;
-const BUTTON_GAP: f32 = 12.0;
-const WIN_W: f32 = 380.0;
-const WIN_H: f32 = 428.0;
+        for j in 0..n_segments {
+            let t0 = j as f32 / n_segments as f32;
+            let t1 = (j + 1) as f32 / n_segments as f32;
+            let mid = (t0 + t1) / 2.0;
+            let c = cce_ui::color::to_linear(get_color_at(mid));
+            quads.push((
+                track_x + t0 * track_w,
+                track_y,
+                (t1 - t0) * track_w,
+                track_h,
+                c,
+            ));
+        }
 
-struct RectWidget {
-    x: f32, y: f32, w: f32, h: f32,
-    color: [f32; 4],
-}
+        // 4. Draw indicator (thumb)
+        let indicator_w = 4.0;
+        let indicator_h = track_h + 4.0;
+        let indicator_x = track_x + self.value * track_w - indicator_w / 2.0;
+        let indicator_y = track_y - 2.0;
+
+        quads.push((
+            indicator_x - 1.0,
+            indicator_y - 1.0,
+            indicator_w + 2.0,
+            indicator_h + 2.0,
+            [0.05, 0.05, 0.05, 0.95],
+        ));
+        quads.push((
+            indicator_x,
+            indicator_y,
+            indicator_w,
+            indicator_h,
+            [1.0, 1.0, 1.0, 1.0],
+        ));
+
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        let (x, y, _, _) = self.rect();
+        let text_y = y + 8.0;
+
+        // Label
+        labels.push(TextLabel {
+            text: self.label.clone(),
+            x: x + SLIDER_LABEL_X,
+            y: text_y,
+            font_size: 12.0,
+            color: [0xaa, 0xaa, 0xbb],
+        });
 
-struct GradientRectWidget {
-    x: f32, y: f32, w: f32, h: f32,
-    c0: [f32; 4],
-    c1: [f32; 4],
-}
+        // Value readout
+        let val_str = if self.channel_index < 3 {
+            format!("{}", (self.value * 255.0) as u8)
+        } else if self.channel_index == 3 {
+            format!("{}°", (self.value * 360.0).round() as u16)
+        } else if self.channel_index < 6 {
+            format!("{}%", (self.value * 100.0).round() as u8)
+        } else {
+            format!("{}", (self.value * 255.0).round() as u8)
+        };
+
+        labels.push(TextLabel {
+            text: val_str,
+            x: x + SLIDER_VALUE_X,
+            y: text_y,
+            font_size: 11.0,
+            color: [0xcc, 0xcc, 0xdd],
+        });
 
-struct TextItem {
-    buffer: Buffer,
-    x: f32, y: f32,
-    color: glyphon::Color,
+        labels
+    }
 }
 
-#[derive(Clone, Copy, PartialEq)]
-enum DragTarget { Red, Green, Blue, Hue, Saturation, Lightness, Alpha }
+// ── AppWidget for flat/rounded rect rendering ────────────────────────
+
+struct AppWidget {
+    x: f32,
+    y: f32,
+    w: f32,
+    h: f32,
+    color: [f32; 4],
+    radius: f32,
+    corners: (bool, bool, bool, bool),
+}
 
-#[derive(Clone, Copy, PartialEq)]
-enum Action { Apply, Cancel }
+// ── Message definition ──────────────────────────────────────────────
 
-struct HitButton {
-    x: f32, y: f32, w: f32, h: f32,
-    action: Action,
+#[derive(Debug, Clone)]
+enum Message {
+    Apply,
+    Cancel,
 }
 
+// ── ColorApp State ───────────────────────────────────────────────────
+
 struct ColorApp {
-    window: XdgWindow,
-    surface: wl_surface::WlSurface,
-    wgpu_surface: wgpu::Surface<'static>,
-    device: wgpu::Device,
-    queue: wgpu::Queue,
-    config: wgpu::SurfaceConfiguration,
-    render_pipeline: wgpu::RenderPipeline,
-    vertex_buffer: wgpu::Buffer,
-    vertex_count: u32,
+    root_window: Window,
+    apply_btn: Button,
+    cancel_btn: Button,
+    sliders: Vec<ColorSlider>,
 
     red: f32,
     green: f32,
@@ -232,368 +345,184 @@ struct ColorApp {
     with_alpha: bool,
 
     font_system: FontSystem,
-    swash_cache: SwashCache,
-    text_atlas: TextAtlas,
-    text_renderer: TextRenderer,
-    text_viewport: Viewport,
-
-    rects: Vec<RectWidget>,
-    gradient_rects: Vec<GradientRectWidget>,
-    text_items: Vec<TextItem>,
-    action_buttons: Vec<HitButton>,
-
     cursor_x: f32,
     cursor_y: f32,
-    dragging: Option<DragTarget>,
-    action_requested: Option<Action>,
-
-    scale_factor: f64,
+    dragging: Option<usize>,
     width: u32,
     height: u32,
+    scale_factor: f64,
     needs_rebuild: bool,
+    ui_context: UiContext,
+
+    widgets: Vec<AppWidget>,
+    text_items: Vec<cce_ui::widget::TextItem>,
 }
 
 impl ColorApp {
-    async fn new(
-        conn: &Connection,
-        qh: &QueueHandle<AppState>,
-        compositor_state: &CompositorState,
-        xdg_shell_state: &XdgShell,
-        red: f32,
-        green: f32,
-        blue: f32,
-        alpha: f32,
-        with_alpha: bool,
-        scale: f64,
-    ) -> Self {
-        let surface = compositor_state.create_surface(qh);
-        surface.set_buffer_scale(scale as i32);
-        let window = xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, qh);
-        window.set_title("Color Interface");
-        window.set_app_id("cce-color-interface");
-        let win_h = if with_alpha { WIN_H + SLIDER_ROW_H } else { WIN_H };
-        window.set_min_size(Some((WIN_W as u32, win_h as u32)));
-        window.commit();
-
-        let wayland_handle = Box::leak(Box::new(cce_ui::wayland::WaylandSurfaceHandle {
-            display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
-            surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
-        }));
-
-        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
-            backends: wgpu::Backends::VULKAN,
-            ..Default::default()
-        });
-        let wgpu_surface = instance.create_surface(wayland_handle).expect("surface");
-        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
-            power_preference: wgpu::PowerPreference::LowPower,
-            compatible_surface: Some(&wgpu_surface),
-            force_fallback_adapter: false,
-        }).await.expect("adapter");
-        let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
-            label: Some("GPU Device"),
-            required_features: wgpu::Features::empty(),
-            required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
-            memory_hints: wgpu::MemoryHints::MemoryUsage,
-        }, None).await.expect("device");
-
-        let scale_factor = scale;
-        let width = (WIN_W * scale_factor as f32) as u32;
-        let height = (win_h * scale_factor as f32) as u32;
-
-        let mut config = wgpu_surface.get_default_config(&adapter, width, height).expect("config");
-        config.width = width;
-        config.height = height;
-        wgpu_surface.configure(&device, &config);
-
-        let shader_code = cce_ui::SHADER;
-        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Shader"),
-            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
-        });
-        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-            label: Some("Pipeline Layout"),
-            bind_group_layouts: &[],
-            push_constant_ranges: &[],
-        });
-        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-            label: Some("Render Pipeline"),
-            layout: Some(&pipeline_layout),
-            vertex: wgpu::VertexState {
-                module: &shader,
-                entry_point: Some("vs_main"),
-                buffers: &[Vertex::desc()],
-                compilation_options: Default::default(),
-            },
-            fragment: Some(wgpu::FragmentState {
-                module: &shader,
-                entry_point: Some("fs_main"),
-                targets: &[Some(wgpu::ColorTargetState {
-                    format: config.format,
-                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
-                    write_mask: wgpu::ColorWrites::ALL,
-                })],
-                compilation_options: Default::default(),
-            }),
-            primitive: wgpu::PrimitiveState {
-                topology: wgpu::PrimitiveTopology::TriangleList,
-                front_face: wgpu::FrontFace::Ccw,
-                cull_mode: None,
-                polygon_mode: wgpu::PolygonMode::Fill,
-                unclipped_depth: false,
-                conservative: false,
-                strip_index_format: None,
-            },
-            depth_stencil: None,
-            multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
-            multiview: None,
-            cache: None,
-        });
-
-        let font_system = FontSystem::new();
-        let swash_cache = SwashCache::new();
-        let cache = Cache::new(&device);
-        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
-        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
-        let mut text_viewport = Viewport::new(&device, &cache);
-        text_viewport.update(&queue, Resolution { width, height });
-
-        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
-            label: Some("Vertex Buffer"),
-            size: 1,
-            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-            mapped_at_creation: false,
-        });
-
-        let (hue, saturation, lightness) = rgb_to_hsl(red, green, blue);
-        let mut app = Self {
-            window, surface, wgpu_surface, device, queue, config, render_pipeline,
-            vertex_buffer, vertex_count: 0,
-            red, green, blue,
-            hue, saturation, lightness,
-            alpha,
-            with_alpha,
-            font_system, swash_cache, text_atlas, text_renderer, text_viewport,
-            rects: Vec::new(),
-            gradient_rects: Vec::new(),
-            text_items: Vec::new(),
-            action_buttons: Vec::new(),
-            cursor_x: 0.0, cursor_y: 0.0,
-            dragging: None,
-            action_requested: None,
-            scale_factor,
-            width, height,
-            needs_rebuild: true,
-        };
-        app.rebuild_layout(width as f32, height as f32);
-        app
-    }
-
     fn hex(&self) -> String {
         if self.with_alpha {
-            format!("#{:02X}{:02X}{:02X}{:02X}",
+            format!(
+                "#{:02X}{:02X}{:02X}{:02X}",
                 (self.red * 255.0) as u8,
                 (self.green * 255.0) as u8,
                 (self.blue * 255.0) as u8,
-                (self.alpha * 255.0) as u8)
+                (self.alpha * 255.0) as u8
+            )
         } else {
-            format!("#{:02X}{:02X}{:02X}",
+            format!(
+                "#{:02X}{:02X}{:02X}",
                 (self.red * 255.0) as u8,
                 (self.green * 255.0) as u8,
-                (self.blue * 255.0) as u8)
+                (self.blue * 255.0) as u8
+            )
         }
     }
 
-    fn rebuild_layout(&mut self, sw: f32, sh: f32) {
-        let s = self.scale_factor as f32;
-        let mut rects = Vec::new();
-        let mut gradient_rects = Vec::new();
-        let mut text_items = Vec::new();
-        let mut action_buttons = Vec::new();
-
-        rects.push(RectWidget {
-            x: 0.0, y: 0.0, w: sw, h: HEADER_H * s,
-            color: color::HEADER_BG,
-        });
-        text_items.push(TextItem {
-            buffer: make_text_buffer(&mut self.font_system, "Color Interface", 14.0 * s),
-            x: 12.0 * s, y: 10.0 * s,
-            color: glyphon::Color::rgb(0xcc, 0xcc, 0xd4),
-        });
-
-        rects.push(RectWidget {
-            x: 0.0, y: HEADER_H * s, w: sw, h: sh - HEADER_H * s,
-            color: color::CONTENT_BG,
-        });
-
-        let mut channels = vec![self.red, self.green, self.blue, self.hue, self.saturation, self.lightness];
-        let mut labels = vec!['R', 'G', 'B', 'H', 'S', 'L'];
-        if self.with_alpha {
-            channels.push(self.alpha);
-            labels.push('A');
+    fn update_sliders_color_state(&mut self) {
+        for slider in &mut self.sliders {
+            slider.r = self.red;
+            slider.g = self.green;
+            slider.b = self.blue;
+            slider.h = self.hue;
+            slider.s = self.saturation;
+            slider.l = self.lightness;
+            slider.a = self.alpha;
         }
+    }
 
-        let red = self.red;
-        let green = self.green;
-        let blue = self.blue;
-        let hue = self.hue;
-        let saturation = self.saturation;
-        let lightness = self.lightness;
-        let alpha = self.alpha;
-
-        let get_color_at = |i: usize, t: f32| -> [f32; 4] {
-            match i {
-                0 => [t, green, blue, alpha],
-                1 => [red, t, blue, alpha],
-                2 => [red, green, t, alpha],
-                3 => {
-                    let (r, g, b) = hsl_to_rgb(t, saturation, lightness);
-                    [r, g, b, alpha]
+    fn update_color_from_slider(&mut self, i: usize, val: f32) {
+        match i {
+            0 => {
+                self.red = val;
+                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+                self.saturation = sat;
+                self.lightness = l;
+                if sat > 0.001 && l > 0.001 && l < 0.999 {
+                    self.hue = h;
                 }
-                4 => {
-                    let (r, g, b) = hsl_to_rgb(hue, t, lightness);
-                    [r, g, b, alpha]
-                }
-                5 => {
-                    let (r, g, b) = hsl_to_rgb(hue, saturation, t);
-                    [r, g, b, alpha]
+            }
+            1 => {
+                self.green = val;
+                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+                self.saturation = sat;
+                self.lightness = l;
+                if sat > 0.001 && l > 0.001 && l < 0.999 {
+                    self.hue = h;
                 }
-                _ => [red, green, blue, t],
             }
-        };
-
-        for i in 0..channels.len() {
-            let row_y = (SLIDER_START_Y + i as f32 * SLIDER_ROW_H) * s;
-            let track_y = row_y + ((SLIDER_ROW_H - SLIDER_TRACK_H) / 2.0) * s;
-
-            // Draw track border
-            rects.push(RectWidget {
-                x: (SLIDER_TRACK_X - 1.0) * s,
-                y: track_y - 1.0 * s,
-                w: (SLIDER_TRACK_W + 2.0) * s,
-                h: (SLIDER_TRACK_H + 2.0) * s,
-                color: color::color_borders_color(),
-            });
-
-            if i == 6 {
-                // Draw checkerboard behind the alpha slider track
-                let track_x = SLIDER_TRACK_X * s;
-                let track_w = SLIDER_TRACK_W * s;
-                let track_h = SLIDER_TRACK_H * s;
-                let grid_size = track_h / 2.0; // Two rows of checkers
-                let cols = (track_w / grid_size).ceil() as i32;
-                
-                // First draw a solid light gray background
-                rects.push(RectWidget {
-                    x: track_x,
-                    y: track_y,
-                    w: track_w,
-                    h: track_h,
-                    color: [0.8, 0.8, 0.8, 1.0],
-                });
-                
-                for r in 0..2 {
-                    for c in 0..cols {
-                        if (r + c) % 2 == 1 {
-                            let qx = track_x + c as f32 * grid_size;
-                            let qy = track_y + r as f32 * grid_size;
-                            let qw = grid_size.min(track_x + track_w - qx);
-                            let qh = grid_size.min(track_y + track_h - qy);
-                            if qw > 0.0 && qh > 0.0 {
-                                rects.push(RectWidget {
-                                    x: qx,
-                                    y: qy,
-                                    w: qw,
-                                    h: qh,
-                                    color: [1.0, 1.0, 1.0, 1.0],
-                                });
-                            }
-                        }
-                    }
+            2 => {
+                self.blue = val;
+                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+                self.saturation = sat;
+                self.lightness = l;
+                if sat > 0.001 && l > 0.001 && l < 0.999 {
+                    self.hue = h;
                 }
             }
-
-            // Draw gradient track
-            let n_segments = if i == 3 { 30 } else { 10 };
-            for j in 0..n_segments {
-                let t0 = j as f32 / n_segments as f32;
-                let t1 = (j + 1) as f32 / n_segments as f32;
-                let c0 = color::to_linear(get_color_at(i, t0));
-                let c1 = color::to_linear(get_color_at(i, t1));
-                gradient_rects.push(GradientRectWidget {
-                    x: (SLIDER_TRACK_X + t0 * SLIDER_TRACK_W) * s,
-                    y: track_y,
-                    w: ((t1 - t0) * SLIDER_TRACK_W) * s,
-                    h: SLIDER_TRACK_H * s,
-                    c0,
-                    c1,
-                });
+            3 => {
+                self.hue = val;
+                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+                self.red = r;
+                self.green = g;
+                self.blue = b;
+            }
+            4 => {
+                self.saturation = val;
+                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+                self.red = r;
+                self.green = g;
+                self.blue = b;
+            }
+            5 => {
+                self.lightness = val;
+                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+                self.red = r;
+                self.green = g;
+                self.blue = b;
             }
+            _ => {
+                self.alpha = val;
+            }
+        }
+        self.sync_slider_values();
+        self.update_sliders_color_state();
+    }
 
-            // Draw vertical bar indicator at current position
-            let indicator_w = 4.0;
-            let indicator_h = SLIDER_TRACK_H + 4.0;
-            let indicator_x = SLIDER_TRACK_X + channels[i] * SLIDER_TRACK_W - indicator_w / 2.0;
-            let indicator_y = (SLIDER_START_Y + i as f32 * SLIDER_ROW_H) + ((SLIDER_ROW_H - indicator_h) / 2.0);
-
-            // Indicator border/shadow
-            rects.push(RectWidget {
-                x: (indicator_x - 1.0) * s,
-                y: (indicator_y - 1.0) * s,
-                w: (indicator_w + 2.0) * s,
-                h: (indicator_h + 2.0) * s,
-                color: [0.05, 0.05, 0.05, 0.95],
-            });
-
-            // Indicator body
-            rects.push(RectWidget {
-                x: indicator_x * s,
-                y: indicator_y * s,
-                w: indicator_w * s,
-                h: indicator_h * s,
-                color: [1.0, 1.0, 1.0, 1.0],
-            });
-
-            text_items.push(TextItem {
-                buffer: make_text_buffer(&mut self.font_system, &labels[i].to_string(), 12.0 * s),
-                x: SLIDER_LABEL_X * s, y: row_y + 4.0 * s,
-                color: glyphon::Color::rgb(0xaa, 0xaa, 0xbb),
-            });
-
-            let val = if i < 3 {
-                format!("{}", (channels[i] * 255.0) as u8)
-            } else if i == 3 {
-                format!("{}°", (channels[i] * 360.0).round() as u16)
-            } else if i < 6 {
-                format!("{}%", (channels[i] * 100.0).round() as u8)
-            } else {
-                format!("{}", (channels[i] * 255.0).round() as u8)
-            };
-            text_items.push(TextItem {
-                buffer: make_text_buffer(&mut self.font_system, &val, 11.0 * s),
-                x: SLIDER_VALUE_X * s, y: row_y + 4.0 * s,
-                color: glyphon::Color::rgb(0xcc, 0xcc, 0xdd),
-            });
+    fn sync_slider_values(&mut self) {
+        self.sliders[0].value = self.red;
+        self.sliders[1].value = self.green;
+        self.sliders[2].value = self.blue;
+        self.sliders[3].value = self.hue;
+        self.sliders[4].value = self.saturation;
+        self.sliders[5].value = self.lightness;
+        if self.with_alpha {
+            self.sliders[6].value = self.alpha;
         }
+    }
+
+    fn rebuild_layout(&mut self) {
+        let mut widgets = Vec::new();
+        let mut text_items = Vec::new();
+
+        // 1. Setup root window
+        self.root_window.set_rect(0.0, 0.0, self.width as f32, self.height as f32);
+        self.root_window.radius = 12.0;
+        self.root_window.background_color = Some(cce_ui::color::page_low_color());
+
+        self.root_window.clear_children(&mut self.ui_context);
 
+        // 2. Link child widgets
+        link_parent_child(&mut self.root_window, &mut self.apply_btn, &mut self.ui_context);
+        link_parent_child(&mut self.root_window, &mut self.cancel_btn, &mut self.ui_context);
+        for slider in &mut self.sliders {
+            link_parent_child(&mut self.root_window, slider, &mut self.ui_context);
+        }
+
+        // 3. Layout elements
         let preview_y_offset = if self.with_alpha { SLIDER_ROW_H } else { 0.0 };
         let preview_y = PREVIEW_Y + preview_y_offset;
         let button_y = BUTTON_Y + preview_y_offset;
 
+        let apply_x = PREVIEW_X;
+        let cancel_x = PREVIEW_X + BUTTON_W + BUTTON_GAP;
+
+        self.apply_btn.set_rect(apply_x, button_y, BUTTON_W, BUTTON_H);
+        self.cancel_btn.set_rect(cancel_x, button_y, BUTTON_W, BUTTON_H);
+
+        for (i, slider) in self.sliders.iter_mut().enumerate() {
+            let row_y = SLIDER_START_Y + i as f32 * SLIDER_ROW_H;
+            slider.set_rect(0.0, row_y, self.width as f32, SLIDER_ROW_H);
+        }
+
+        // 4. Render window widget recursively
+        let mut window_pc = PageContent::new();
+        cce_ui::layout::render_widget(
+            &mut window_pc,
+            &mut self.root_window,
+            0.0,
+            0.0,
+            self.width as f32,
+            self.height as f32,
+            &mut self.ui_context,
+        );
+
+        // 5. Render custom elements
+        let mut custom_pc = PageContent::new();
+
+        // Header title
+        custom_pc.rect(cce_ui::color::HEADER_BG, 0.0, 0.0, self.width as f32, HEADER_H);
+        custom_pc.text("Color Interface", 12.0, 10.0, 14.0, [0.8, 0.8, 0.83, 1.0]);
+
         if self.with_alpha {
             // Draw checkerboard behind the preview box
-            let px = PREVIEW_X * s;
-            let py = preview_y * s;
-            let pw = PREVIEW_W * s;
-            let ph = PREVIEW_H * s;
-            
-            // Draw base light gray
-            rects.push(RectWidget {
-                x: px, y: py, w: pw, h: ph,
-                color: [0.8, 0.8, 0.8, 1.0],
-            });
-            
-            let grid_size = 12.0 * s;
+            let px = PREVIEW_X;
+            let py = preview_y;
+            let pw = PREVIEW_W;
+            let ph = PREVIEW_H;
+
+            custom_pc.rect([0.8, 0.8, 0.8, 1.0], px, py, pw, ph);
+
+            let grid_size = 12.0;
             let cols = (pw / grid_size).ceil() as i32;
             let rows = (ph / grid_size).ceil() as i32;
             for r in 0..rows {
@@ -604,874 +533,393 @@ impl ColorApp {
                         let qw = grid_size.min(px + pw - qx);
                         let qh = grid_size.min(py + ph - qy);
                         if qw > 0.0 && qh > 0.0 {
-                            rects.push(RectWidget {
-                                x: qx, y: qy, w: qw, h: qh,
-                                color: [1.0, 1.0, 1.0, 1.0],
-                            });
+                            custom_pc.rect([1.0, 1.0, 1.0, 1.0], qx, qy, qw, qh);
                         }
                     }
                 }
             }
         }
 
-        rects.push(RectWidget {
-            x: PREVIEW_X * s, y: preview_y * s,
-            w: PREVIEW_W * s, h: PREVIEW_H * s,
-            color: color::to_linear([self.red, self.green, self.blue, if self.with_alpha { self.alpha } else { 1.0 }]),
-        });
+        // Color block preview
+        let linear_col = cce_ui::color::to_linear([
+            self.red,
+            self.green,
+            self.blue,
+            if self.with_alpha { self.alpha } else { 1.0 },
+        ]);
+        custom_pc.rect(linear_col, PREVIEW_X, preview_y, PREVIEW_W, PREVIEW_H);
 
+        // Hex string readout
         let hex = self.hex();
-        text_items.push(TextItem {
-            buffer: make_text_buffer(&mut self.font_system, &hex, 16.0 * s),
-            x: (PREVIEW_X + PREVIEW_W + 16.0) * s,
-            y: (preview_y + 26.0) * s,
-            color: glyphon::Color::rgb(0xe0, 0xe0, 0xe8),
-        });
-
-        // Apply button
-        let apply_x = PREVIEW_X;
-        let cancel_x = PREVIEW_X + BUTTON_W + BUTTON_GAP;
-        let btn_y = button_y;
-        let btn_bg = [0.20, 0.40, 0.65, 1.0];
-        let cancel_bg = [0.40, 0.20, 0.20, 1.0];
-
-        rects.push(RectWidget {
-            x: apply_x * s, y: btn_y * s,
-            w: BUTTON_W * s, h: BUTTON_H * s,
-            color: btn_bg,
-        });
-        text_items.push(TextItem {
-            buffer: make_text_buffer(&mut self.font_system, "Apply", 12.0 * s),
-            x: (apply_x + 28.0) * s, y: (btn_y + 8.0) * s,
-            color: glyphon::Color::rgb(0xee, 0xee, 0xf0),
-        });
-        action_buttons.push(HitButton {
-            x: apply_x * s, y: btn_y * s,
-            w: BUTTON_W * s, h: BUTTON_H * s,
-            action: Action::Apply,
-        });
-
-        rects.push(RectWidget {
-            x: cancel_x * s, y: btn_y * s,
-            w: BUTTON_W * s, h: BUTTON_H * s,
-            color: cancel_bg,
-        });
-        text_items.push(TextItem {
-            buffer: make_text_buffer(&mut self.font_system, "Cancel", 12.0 * s),
-            x: (cancel_x + 22.0) * s, y: (btn_y + 8.0) * s,
-            color: glyphon::Color::rgb(0xee, 0xee, 0xf0),
-        });
-        action_buttons.push(HitButton {
-            x: cancel_x * s, y: btn_y * s,
-            w: BUTTON_W * s, h: BUTTON_H * s,
-            action: Action::Cancel,
-        });
+        custom_pc.text(
+            &hex,
+            PREVIEW_X + PREVIEW_W + 16.0,
+            preview_y + 26.0,
+            16.0,
+            [0.88, 0.88, 0.91, 1.0],
+        );
+
+        // 6. Gather all quads and text labels
+        for pc_part in &[window_pc, custom_pc] {
+            for (c, x, y, w, h, r, corners) in &pc_part.rects {
+                widgets.push(AppWidget {
+                    x: *x,
+                    y: *y,
+                    w: *w,
+                    h: *h,
+                    color: *c,
+                    radius: *r,
+                    corners: *corners,
+                });
+            }
+            for (text, size, x, y, col, font, bounds) in &pc_part.texts {
+                text_items.push(cce_ui::widget::TextItem::new(
+                    &mut self.font_system,
+                    text,
+                    *size,
+                    *x,
+                    *y,
+                    glyphon::Color::rgb(
+                        (col[0] * 255.0) as u8,
+                        (col[1] * 255.0) as u8,
+                        (col[2] * 255.0) as u8,
+                    ),
+                    font.as_deref(),
+                    *bounds,
+                ));
+            }
+        }
 
-        self.rects = rects;
-        self.gradient_rects = gradient_rects;
+        self.widgets = widgets;
         self.text_items = text_items;
-        self.action_buttons = action_buttons;
         self.needs_rebuild = false;
     }
+}
 
-    fn slider_physical_rect(i: usize, s: f32) -> (f32, f32, f32, f32) {
-        let row_y = (SLIDER_START_Y + i as f32 * SLIDER_ROW_H) * s;
-        let track_y = row_y + ((SLIDER_ROW_H - SLIDER_TRACK_H) / 2.0) * s;
-        (SLIDER_TRACK_X * s, track_y, SLIDER_TRACK_W * s, SLIDER_TRACK_H * s)
-    }
-
-    fn collect_vertices(&self) -> Vec<Vertex> {
-        let sw = self.width as f32;
-        let sh = self.height as f32;
-        let mut verts = Vec::new();
-        for r in &self.rects {
-            verts.extend(quad_vertices(r.x, r.y, r.w, r.h, sw, sh, r.color));
-        }
-        for g in &self.gradient_rects {
-            verts.extend(gradient_quad_vertices(g.x, g.y, g.w, g.h, sw, sh, g.c0, g.c1));
-        }
-        verts
-    }
-
-    fn upload_vertices(&mut self) {
-        let verts = self.collect_vertices();
-        self.vertex_count = verts.len() as u32;
-        let data = bytemuck::cast_slice(&verts);
-        let needed = data.len() as wgpu::BufferAddress;
-        if needed > self.vertex_buffer.size() {
-            self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
-                label: Some("Vertex Buffer"),
-                size: needed,
-                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                mapped_at_creation: false,
-            });
-        }
-        self.queue.write_buffer(&self.vertex_buffer, 0, data);
-    }
+// ── Application Trait Implementation ────────────────────────────────
 
-    fn prepare_text(&mut self) {
-        let Self {
-            ref mut text_renderer, ref device, ref queue,
-            ref mut font_system, ref mut text_atlas,
-            ref mut text_viewport, ref mut swash_cache,
-            ref text_items, width, height, ..
-        } = self;
-
-        let w = *width as f32;
-        let h = *height as f32;
-        let viewport = Resolution { width: w as u32, height: h as u32 };
-        text_viewport.update(queue, viewport);
-        let bounds = TextBounds { left: 0, top: 0, right: w as i32, bottom: h as i32 };
-        let areas: Vec<TextArea> = text_items.iter().map(|ti| TextArea {
-            buffer: &ti.buffer,
-            left: ti.x.round(), top: ti.y.round(), scale: 1.0, bounds,
-            default_color: ti.color,
-            custom_glyphs: &[],
-        }).collect();
-        text_renderer.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache).unwrap();
-    }
-
-    fn handle_cursor_moved(&mut self, cx: f32, cy: f32) {
-        self.cursor_x = cx;
-        self.cursor_y = cy;
-        if let Some(drag) = self.dragging {
-            let i = match drag {
-                DragTarget::Red => 0,
-                DragTarget::Green => 1,
-                DragTarget::Blue => 2,
-                DragTarget::Hue => 3,
-                DragTarget::Saturation => 4,
-                DragTarget::Lightness => 5,
-                DragTarget::Alpha => 6,
-            };
-            let s = self.scale_factor as f32;
-            let (tx, _, tw, _) = Self::slider_physical_rect(i, s);
-            let new_val = ((self.cursor_x - tx) / tw).clamp(0.0, 1.0);
-            let old = match i {
-                0 => self.red,
-                1 => self.green,
-                2 => self.blue,
-                3 => self.hue,
-                4 => self.saturation,
-                5 => self.lightness,
-                _ => self.alpha,
-            };
-            if (new_val - old).abs() > 0.002 {
-                match i {
-                    0 => {
-                        self.red = new_val;
-                        let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                        self.saturation = sat;
-                        self.lightness = l;
-                        if sat > 0.001 && l > 0.001 && l < 0.999 {
-                            self.hue = h;
-                        }
-                    }
-                    1 => {
-                        self.green = new_val;
-                        let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                        self.saturation = sat;
-                        self.lightness = l;
-                        if sat > 0.001 && l > 0.001 && l < 0.999 {
-                            self.hue = h;
-                        }
-                    }
-                    2 => {
-                        self.blue = new_val;
-                        let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                        self.saturation = sat;
-                        self.lightness = l;
-                        if sat > 0.001 && l > 0.001 && l < 0.999 {
-                            self.hue = h;
-                        }
-                    }
-                    3 => {
-                        self.hue = new_val;
-                        let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                        self.red = r;
-                        self.green = g;
-                        self.blue = b;
-                    }
-                    4 => {
-                        self.saturation = new_val;
-                        let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                        self.red = r;
-                        self.green = g;
-                        self.blue = b;
-                    }
-                    5 => {
-                        self.lightness = new_val;
-                        let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                        self.red = r;
-                        self.green = g;
-                        self.blue = b;
-                    }
-                    _ => {
-                        self.alpha = new_val;
-                    }
-                }
-                self.needs_rebuild = true;
-            }
-        }
-    }
+impl Application for ColorApp {
+    type Message = Message;
 
-    fn handle_mouse_input(&mut self, state: cce_ui::widget::ElementState) {
-        match state {
-            cce_ui::widget::ElementState::Pressed => {
-                let s = self.scale_factor as f32;
-                let (px, py) = (self.cursor_x, self.cursor_y);
-                let num_sliders = if self.with_alpha { 7 } else { 6 };
-                for i in 0..num_sliders {
-                    let (tx, ty, tw, th) = Self::slider_physical_rect(i, s);
-                    if px >= tx && px <= tx + tw && py >= ty && py <= ty + th {
-                        let val = ((px - tx) / tw).clamp(0.0, 1.0);
-                        match i {
-                            0 => {
-                                self.red = val;
-                                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                                self.saturation = sat;
-                                self.lightness = l;
-                                if sat > 0.001 && l > 0.001 && l < 0.999 {
-                                    self.hue = h;
-                                }
-                                self.dragging = Some(DragTarget::Red);
-                            }
-                            1 => {
-                                self.green = val;
-                                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                                self.saturation = sat;
-                                self.lightness = l;
-                                if sat > 0.001 && l > 0.001 && l < 0.999 {
-                                    self.hue = h;
-                                }
-                                self.dragging = Some(DragTarget::Green);
-                            }
-                            2 => {
-                                self.blue = val;
-                                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                                self.saturation = sat;
-                                self.lightness = l;
-                                if sat > 0.001 && l > 0.001 && l < 0.999 {
-                                    self.hue = h;
-                                }
-                                self.dragging = Some(DragTarget::Blue);
-                            }
-                            3 => {
-                                self.hue = val;
-                                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                                self.red = r;
-                                self.green = g;
-                                self.blue = b;
-                                self.dragging = Some(DragTarget::Hue);
-                            }
-                            4 => {
-                                self.saturation = val;
-                                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                                self.red = r;
-                                self.green = g;
-                                self.blue = b;
-                                self.dragging = Some(DragTarget::Saturation);
-                            }
-                            5 => {
-                                self.lightness = val;
-                                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                                self.red = r;
-                                self.green = g;
-                                self.blue = b;
-                                self.dragging = Some(DragTarget::Lightness);
-                            }
-                            _ => {
-                                self.alpha = val;
-                                self.dragging = Some(DragTarget::Alpha);
-                            }
-                        }
-                        self.needs_rebuild = true;
-                        return;
-                    }
-                }
-                for btn in &self.action_buttons {
-                    if px >= btn.x && px <= btn.x + btn.w && py >= btn.y && py <= btn.y + btn.h {
-                        self.action_requested = Some(btn.action);
-                        self.needs_rebuild = true;
-                        return;
-                    }
-                }
-            }
-            cce_ui::widget::ElementState::Released => {
-                if self.dragging.is_some() {
-                    self.dragging = None;
-                }
+    fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
+        let args: Vec<String> = std::env::args().collect();
+        let mut with_alpha = false;
+        let mut hex_arg = None;
+        for arg in args.iter().skip(1) {
+            if arg == "--alpha" || arg == "-a" {
+                with_alpha = true;
+            } else {
+                hex_arg = Some(arg.as_str());
             }
         }
-    }
 
-    fn handle_scroll(&mut self, _scroll_amount_x: f32, scroll_amount_y: f32) {
-        let s = self.scale_factor as f32;
-        let (px, py) = (self.cursor_x, self.cursor_y);
-        let scroll_amount = scroll_amount_y;
-        if scroll_amount.abs() > 0.0001 {
-            let num_sliders = if self.with_alpha { 7 } else { 6 };
-            for i in 0..num_sliders {
-                let (tx, ty, tw, th) = Self::slider_physical_rect(i, s);
-                if px >= tx && px <= tx + tw && py >= ty - 4.0 * s && py <= ty + th + 4.0 * s {
-                    let step = 0.02;
-                    let old_val = match i {
-                        0 => self.red,
-                        1 => self.green,
-                        2 => self.blue,
-                        3 => self.hue,
-                        4 => self.saturation,
-                        5 => self.lightness,
-                        _ => self.alpha,
-                    };
-                    let new_val = (old_val + scroll_amount * step).clamp(0.0, 1.0);
-                    if (new_val - old_val).abs() > 0.0001 {
-                        match i {
-                            0 => {
-                                self.red = new_val;
-                                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                                self.saturation = sat;
-                                self.lightness = l;
-                                if sat > 0.001 && l > 0.001 && l < 0.999 {
-                                    self.hue = h;
-                                }
-                            }
-                            1 => {
-                                self.green = new_val;
-                                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                                self.saturation = sat;
-                                self.lightness = l;
-                                if sat > 0.001 && l > 0.001 && l < 0.999 {
-                                    self.hue = h;
-                                }
-                            }
-                            2 => {
-                                self.blue = new_val;
-                                let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
-                                self.saturation = sat;
-                                self.lightness = l;
-                                if sat > 0.001 && l > 0.001 && l < 0.999 {
-                                    self.hue = h;
-                                }
-                            }
-                            3 => {
-                                self.hue = new_val;
-                                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                                self.red = r;
-                                self.green = g;
-                                self.blue = b;
-                            }
-                            4 => {
-                                self.saturation = new_val;
-                                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                                self.red = r;
-                                self.green = g;
-                                self.blue = b;
-                            }
-                            5 => {
-                                self.lightness = new_val;
-                                let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
-                                self.red = r;
-                                self.green = g;
-                                self.blue = b;
-                            }
-                            _ => {
-                                self.alpha = new_val;
-                            }
-                        }
-                        self.needs_rebuild = true;
-                    }
+        let (r, g, b, a) = if let Some(hex) = hex_arg {
+            if let Some((r_parsed, g_parsed, b_parsed, parsed_a)) = parse_hex(hex) {
+                if parsed_a.is_some() {
+                    with_alpha = true;
                 }
+                (r_parsed, g_parsed, b_parsed, parsed_a.unwrap_or(1.0))
+            } else {
+                (0.5, 0.5, 0.5, 1.0)
             }
-        }
-    }
-
-    fn resize(&mut self, width: u32, height: u32) {
-        if width > 0 && height > 0 {
-            self.width = width;
-            self.height = height;
-            self.config.width = width;
-            self.config.height = height;
-            self.wgpu_surface.configure(&self.device, &self.config);
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn render(&mut self) {
-        let sw = self.width as f32;
-        let sh = self.height as f32;
+        } else {
+            (0.5, 0.5, 0.5, 1.0)
+        };
 
-        if self.needs_rebuild {
-            self.rebuild_layout(sw, sh);
-            self.upload_vertices();
+        let (hue, saturation, lightness) = rgb_to_hsl(r, g, b);
+
+        let initial_w = 380;
+        let initial_h = if with_alpha { 464 } else { 428 };
+
+        let root_window = Window::new(0.0, 0.0, initial_w as f32, initial_h as f32);
+
+        let apply_btn = Button::new(0.0, 0.0, BUTTON_W, BUTTON_H)
+            .with_label("Apply")
+            .with_bg([0.20, 0.40, 0.65, 1.0])
+            .with_hover_bg([0.30, 0.52, 0.78, 1.0])
+            .with_label_color([0.93, 0.93, 0.94, 1.0]);
+
+        let cancel_btn = Button::new(0.0, 0.0, BUTTON_W, BUTTON_H)
+            .with_label("Cancel")
+            .with_bg([0.40, 0.20, 0.20, 1.0])
+            .with_hover_bg([0.55, 0.20, 0.20, 1.0])
+            .with_label_color([0.93, 0.93, 0.94, 1.0]);
+
+        let mut sliders = vec![
+            ColorSlider::new("R", 0),
+            ColorSlider::new("G", 1),
+            ColorSlider::new("B", 2),
+            ColorSlider::new("H", 3),
+            ColorSlider::new("S", 4),
+            ColorSlider::new("L", 5),
+        ];
+        if with_alpha {
+            sliders.push(ColorSlider::new("A", 6));
         }
 
-        self.prepare_text();
+        let font_system = FontSystem::new();
 
-        let output = match self.wgpu_surface.get_current_texture() {
-            Ok(t) => t,
-            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
-                self.wgpu_surface.configure(&self.device, &self.config);
-                return;
-            }
-            Err(wgpu::SurfaceError::Timeout) => return,
-            Err(e) => { eprintln!("Surface error: {e:?}"); return; }
+        let mut app = Self {
+            root_window,
+            apply_btn,
+            cancel_btn,
+            sliders,
+            red: r,
+            green: g,
+            blue: b,
+            hue,
+            saturation,
+            lightness,
+            alpha: a,
+            with_alpha,
+            font_system,
+            cursor_x: 0.0,
+            cursor_y: 0.0,
+            dragging: None,
+            width: initial_w,
+            height: initial_h,
+            scale_factor: 1.0,
+            needs_rebuild: true,
+            ui_context: UiContext::new(),
+            widgets: Vec::new(),
+            text_items: Vec::new(),
         };
 
-        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
-        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
-            label: Some("Encoder"),
-        });
+        app.sync_slider_values();
+        app.update_sliders_color_state();
+        app.rebuild_layout();
+        app
+    }
 
-        let bg = color::page_low_color();
-        let clear_color = wgpu::Color {
-            r: bg[0] as f64,
-            g: bg[1] as f64,
-            b: bg[2] as f64,
-            a: bg[3] as f64,
-        };
-        {
-            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("Render Pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: &view,
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Clear(clear_color),
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: None,
-                timestamp_writes: None,
-                occlusion_query_set: None,
-            });
-
-            pass.set_pipeline(&self.render_pipeline);
-            pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
-            pass.draw(0..self.vertex_count, 0..1);
-
-            self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
+    fn settings(&self) -> WindowSettings {
+        let win_h = if self.with_alpha { 464 } else { 428 };
+        WindowSettings {
+            title: "Color Interface".to_string(),
+            app_id: "cce-color-interface".to_string(),
+            width: 380,
+            height: win_h,
+            fullscreen: false,
+            min_size: Some((380, win_h)),
         }
-
-        self.queue.submit(std::iter::once(encoder.finish()));
-        output.present();
     }
-}
-
-struct AppState {
-    registry_state: RegistryState,
-    compositor_state: CompositorState,
-    xdg_shell_state: XdgShell,
-    shm_state: Shm,
-    seat_state: SeatState,
-    output_state: OutputState,
 
-    seats: Vec<wl_seat::WlSeat>,
-    pointer: Option<wl_pointer::WlPointer>,
-    keyboard: Option<wl_keyboard::WlKeyboard>,
-
-    window: Option<XdgWindow>,
-    surface: Option<wl_surface::WlSurface>,
-
-    state: Option<ColorApp>,
-    exit: bool,
-    redraw: bool,
-}
-
-impl CompositorHandler for AppState {
-    fn scale_factor_changed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        scale_factor: i32,
-    ) {
-        _surface.set_buffer_scale(scale_factor);
-        if let Some(state) = &mut self.state {
-            state.scale_factor = scale_factor as f64;
-            let win_h = if state.with_alpha { WIN_H + SLIDER_ROW_H } else { WIN_H };
-            state.resize((WIN_W * state.scale_factor as f32) as u32, (win_h * state.scale_factor as f32) as u32);
-            self.redraw = true;
+    fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
+        match msg {
+            Message::Apply => {
+                println!("{}", self.hex());
+                *exit = true;
+            }
+            Message::Cancel => {
+                *exit = true;
+            }
         }
     }
 
-    fn transform_changed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _new_transform: wl_output::Transform,
-    ) {
-    }
+    fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
 
-    fn frame(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _time: u32,
-    ) {
-    }
+    fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
 
-    fn surface_enter(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _output: &wl_output::WlOutput,
-    ) {
+    fn view(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
+        if self.needs_rebuild || self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale {
+            self.width = size.width as u32;
+            self.height = size.height as u32;
+            self.scale_factor = scale;
+            cce_ui::scale::set_scale_factor(scale as f32);
+            self.rebuild_layout();
+        }
     }
 
-    fn surface_leave(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _surface: &wl_surface::WlSurface,
-        _output: &wl_output::WlOutput,
-    ) {
+    fn view_rounded_quads(&mut self, quads: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))>, size: LogicalSize, scale: f64) {
+        if self.needs_rebuild || self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale {
+            self.width = size.width as u32;
+            self.height = size.height as u32;
+            self.scale_factor = scale;
+            cce_ui::scale::set_scale_factor(scale as f32);
+            self.rebuild_layout();
+        }
+        for w in &self.widgets {
+            quads.push((w.x, w.y, w.w, w.h, w.radius, w.color, w.corners));
+        }
     }
-}
 
-impl OutputHandler for AppState {
-    fn output_state(&mut self) -> &mut OutputState {
-        &mut self.output_state
+    fn text_items(&self) -> &[cce_ui::widget::TextItem] {
+        &self.text_items
     }
 
-    fn new_output(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _output: wl_output::WlOutput,
-    ) {
+    fn clear_color(&self) -> [f32; 4] {
+        cce_ui::color::page_low_color()
     }
 
-    fn update_output(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _output: wl_output::WlOutput,
-    ) {
-    }
+    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        self.cursor_x = pos.x;
+        self.cursor_y = pos.y;
+        cce_ui::widget::hover_animation::set_cursor_pos(pos.x, pos.y);
 
-    fn output_destroyed(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _output: wl_output::WlOutput,
-    ) {
+        if let Some(i) = self.dragging {
+            let slider = &mut self.sliders[i];
+            if slider.drag_update(pos.x, pos.y) {
+                let val = slider.value;
+                self.update_color_from_slider(i, val);
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        } else {
+            let _ = self.apply_btn.cursor_moved(pos.x, pos.y, &mut self.ui_context);
+            let _ = self.cancel_btn.cursor_moved(pos.x, pos.y, &mut self.ui_context);
+            for slider in &mut self.sliders {
+                let _ = slider.cursor_moved(pos.x, pos.y, &mut self.ui_context);
+            }
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
     }
-}
 
-impl SeatHandler for AppState {
-    fn seat_state(&mut self) -> &mut SeatState {
-        &mut self.seat_state
-    }
+    fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        if button != MouseButton::Left {
+            return None;
+        }
 
-    fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
-        self.seats.push(seat);
-    }
+        let px = pos.x;
+        let py = pos.y;
+
+        let mut handled = false;
 
-    fn new_capability(
-        &mut self,
-        _conn: &Connection,
-        qh: &QueueHandle<Self>,
-        seat: wl_seat::WlSeat,
-        capability: Capability,
-    ) {
-        if capability == Capability::Pointer && self.pointer.is_none() {
-            let pointer = self.seat_state.get_pointer(qh, &seat).unwrap();
-            self.pointer = Some(pointer);
+        if self.apply_btn.mouse_input(button, state, px, py, &mut self.ui_context) {
+            handled = true;
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
         }
-        if capability == Capability::Keyboard && self.keyboard.is_none() {
-            let keyboard = self
-                .seat_state
-                .get_keyboard(qh, &seat, None)
-                .unwrap();
-            self.keyboard = Some(keyboard);
+        if self.cancel_btn.mouse_input(button, state, px, py, &mut self.ui_context) {
+            handled = true;
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
         }
-    }
 
-    fn remove_capability(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _seat: wl_seat::WlSeat,
-        capability: Capability,
-    ) {
-        if capability == Capability::Pointer {
-            self.pointer = None;
+        if self.apply_btn.take_click() {
+            return Some(Message::Apply);
         }
-        if capability == Capability::Keyboard {
-            self.keyboard = None;
+        if self.cancel_btn.take_click() {
+            return Some(Message::Cancel);
         }
-    }
 
-    fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
-        self.seats.retain(|s| s != &seat);
-    }
-}
+        if state == ElementState::Released {
+            if let Some(i) = self.dragging {
+                self.sliders[i].drag_end();
+                self.dragging = None;
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
 
-impl ShmHandler for AppState {
-    fn shm_state(&mut self) -> &mut Shm {
-        &mut self.shm_state
+        if !handled {
+            for (i, slider) in self.sliders.iter_mut().enumerate() {
+                if slider.mouse_input(button, state, px, py, &mut self.ui_context) {
+                    if slider.dragging {
+                        self.dragging = Some(i);
+                    } else {
+                        self.dragging = None;
+                    }
+                    let val = slider.value;
+                    self.update_color_from_slider(i, val);
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                    break;
+                }
+            }
+        }
+
+        None
     }
-}
 
-impl PointerHandler for AppState {
-    fn pointer_frame(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _pointer: &wl_pointer::WlPointer,
-        events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
-    ) {
-        use smithay_client_toolkit::seat::pointer::PointerEventKind;
-        for event in events {
-            if let Some(st) = &mut self.state {
-                let (cx, cy) = cce_ui::wayland::scale_pointer_pos(event.position, st.scale_factor);
-                match &event.kind {
-                    PointerEventKind::Motion { .. } => {
-                        st.handle_cursor_moved(cx, cy);
-                        self.redraw = true;
-                    }
-                    PointerEventKind::Press { button, .. } => {
-                        if *button == 272 {
-                            st.cursor_x = cx;
-                            st.cursor_y = cy;
-                            st.handle_mouse_input(cce_ui::widget::ElementState::Pressed);
-                            self.redraw = true;
-                        }
-                    }
-                    PointerEventKind::Release { button, .. } => {
-                        if *button == 272 {
-                            st.cursor_x = cx;
-                            st.cursor_y = cy;
-                            st.handle_mouse_input(cce_ui::widget::ElementState::Released);
-                            self.redraw = true;
-                        }
-                    }
-                    PointerEventKind::Axis { horizontal, vertical, .. } => {
-                        let h_scroll = horizontal.absolute as f32;
-                        let v_scroll = vertical.absolute as f32;
-                        st.handle_scroll(-h_scroll / 10.0, -v_scroll / 10.0);
-                        self.redraw = true;
-                    }
-                    _ => {}
+    fn handle_key_input(&mut self, event: &KeyEvent, _needs_rebuild: &mut bool) -> Option<Self::Message> {
+        if event.state == ElementState::Pressed {
+            match event.logical_key {
+                cce_ui::widget::Key::Named(cce_ui::widget::NamedKey::Escape) => {
+                    return Some(Message::Cancel);
                 }
+                cce_ui::widget::Key::Named(cce_ui::widget::NamedKey::Enter) => {
+                    return Some(Message::Apply);
+                }
+                _ => {}
             }
         }
+        None
     }
 }
 
-impl KeyboardHandler for AppState {
-    fn enter(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _surface: &wl_surface::WlSurface,
-        _serial: u32,
-        _raw_modifiers: &[u32],
-        _keysyms: &[xkeysym::Keysym],
-    ) {}
-
-    fn leave(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _surface: &wl_surface::WlSurface,
-        _serial: u32,
-    ) {}
-
-    fn press_key(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        _event: smithay_client_toolkit::seat::keyboard::KeyEvent,
-    ) {}
-
-    fn release_key(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        _event: smithay_client_toolkit::seat::keyboard::KeyEvent,
-    ) {}
-
-    fn update_modifiers(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _keyboard: &wl_keyboard::WlKeyboard,
-        _serial: u32,
-        _modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
-        _layout: u32,
-    ) {}
-}
+// ── Color Utilities ──────────────────────────────────────────────────
 
-impl WindowHandler for AppState {
-    fn configure(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _window: &XdgWindow,
-        configure: WindowConfigure,
-        _serial: u32,
-    ) {
-        let (w, h) = configure.new_size;
-        if let (Some(w), Some(h)) = (w, h) {
-            let width = w.get();
-            let height = h.get();
-            if let Some(state) = &mut self.state {
-                let pw = (width as f64 * state.scale_factor) as u32;
-                let ph = (height as f64 * state.scale_factor) as u32;
-                state.resize(pw, ph);
-            }
+fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
+    let max = r.max(g.max(b));
+    let min = r.min(g.min(b));
+    let mut h = 0.0;
+    let mut s = 0.0;
+    let l = (max + min) / 2.0;
+
+    if max != min {
+        let d = max - min;
+        s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
+        if max == r {
+            h = (g - b) / d + (if g < b { 6.0 } else { 0.0 });
+        } else if max == g {
+            h = (b - r) / d + 2.0;
+        } else if max == b {
+            h = (r - g) / d + 4.0;
         }
-        self.redraw = true;
+        h /= 6.0;
     }
 
-    fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
-        self.exit = true;
-    }
+    (h, s, l)
 }
 
-impl ProvidesRegistryState for AppState {
-    fn registry(&mut self) -> &mut RegistryState {
-        &mut self.registry_state
+fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
+    if s == 0.0 {
+        return (l, l, l);
     }
-    
-    fn runtime_add_global(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _name: u32,
-        _interface: &str,
-        _version: u32,
-    ) {}
-    
-    fn runtime_remove_global(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _name: u32,
-        _interface: &str,
-    ) {}
-}
 
-delegate_compositor!(AppState);
-delegate_xdg_shell!(AppState);
-delegate_xdg_window!(AppState);
-delegate_shm!(AppState);
-delegate_seat!(AppState);
-delegate_pointer!(AppState);
-delegate_keyboard!(AppState);
-delegate_registry!(AppState);
-delegate_output!(AppState);
+    let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
+    let p = 2.0 * l - q;
+
+    let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
+    let g = hue_to_rgb(p, q, h);
+    let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
 
-fn main() {
-    let args: Vec<String> = std::env::args().collect();
-    let mut with_alpha = false;
-    let mut hex_arg = None;
-    for arg in args.iter().skip(1) {
-        if arg == "--alpha" || arg == "-a" {
-            with_alpha = true;
-        } else {
-            hex_arg = Some(arg.as_str());
-        }
-    }
+    (r, g, b)
+}
 
-    let (r, g, b, a) = if let Some(hex) = hex_arg {
-        if let Some((r_parsed, g_parsed, b_parsed, parsed_a)) = parse_hex(hex) {
-            if parsed_a.is_some() {
-                with_alpha = true;
-            }
-            (r_parsed, g_parsed, b_parsed, parsed_a.unwrap_or(1.0))
-        } else {
-            (0.5, 0.5, 0.5, 1.0)
-        }
+fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
+    if t < 0.0 { t += 1.0; }
+    if t > 1.0 { t -= 1.0; }
+    if t < 1.0 / 6.0 { return p + (q - p) * 6.0 * t; }
+    if t < 1.0 / 2.0 { return q; }
+    if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
+    p
+}
+
+fn parse_hex(hex: &str) -> Option<(f32, f32, f32, Option<f32>)> {
+    let s = hex.trim_start_matches('#');
+    if s.len() == 6 {
+        u32::from_str_radix(s, 16).ok().map(|v| {
+            let r = ((v >> 16) & 0xFF) as f32 / 255.0;
+            let g = ((v >> 8) & 0xFF) as f32 / 255.0;
+            let b = (v & 0xFF) as f32 / 255.0;
+            (r, g, b, None)
+        })
+    } else if s.len() == 8 {
+        u32::from_str_radix(s, 16).ok().map(|v| {
+            let r = ((v >> 24) & 0xFF) as f32 / 255.0;
+            let g = ((v >> 16) & 0xFF) as f32 / 255.0;
+            let b = ((v >> 8) & 0xFF) as f32 / 255.0;
+            let a = (v & 0xFF) as f32 / 255.0;
+            (r, g, b, Some(a))
+        })
     } else {
-        (0.5, 0.5, 0.5, 1.0)
-    };
-
-    let conn = Connection::connect_to_env().unwrap();
-    let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
-    let qh = event_queue.handle();
-
-    let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
-    let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
-    let shm_state = Shm::bind(&globals, &qh).unwrap();
-    let seat_state = SeatState::new(&globals, &qh);
-    let output_state = OutputState::new(&globals, &qh);
-
-    let mut app = AppState {
-        registry_state: RegistryState::new(&globals),
-        compositor_state,
-        xdg_shell_state,
-        shm_state,
-        seat_state,
-        output_state,
-        seats: Vec::new(),
-        pointer: None,
-        keyboard: None,
-        window: None,
-        surface: None,
-        state: None,
-        exit: false,
-        redraw: true,
-    };
-
-    // Perform a roundtrip to populate output_state with active output scales
-    event_queue.roundtrip(&mut app).unwrap();
-
-    let scale = cce_ui::wayland::detect_scale_factor(&app.output_state);
-
-    let state = pollster::block_on(ColorApp::new(
-        &conn,
-        &qh,
-        &app.compositor_state,
-        &app.xdg_shell_state,
-        r, g, b, a,
-        with_alpha,
-        scale,
-    ));
-
-    app.window = Some(state.window.clone());
-    app.surface = Some(state.surface.clone());
-    app.state = Some(state);
-
-    let mut event_loop = EventLoop::try_new().unwrap();
-    let loop_handle = event_loop.handle();
-    WaylandSource::new(conn, event_queue).insert(loop_handle).unwrap();
-
-    loop {
-        event_loop
-            .dispatch(std::time::Duration::from_millis(16), &mut app)
-            .unwrap();
-        if app.exit {
-            break;
-        }
-        if let Some(ref state) = app.state {
-            if let Some(action) = state.action_requested {
-                match action {
-                    Action::Apply => {
-                        println!("{}", state.hex());
-                    }
-                    Action::Cancel => {}
-                }
-                break;
-            }
-        }
-        if app.redraw {
-            app.redraw = false;
-            if let Some(state) = &mut app.state {
-                state.render();
-            }
-        }
+        None
     }
-    drop(app);
+}
+
+// ── Main Entrypoint ──────────────────────────────────────────────────
+
+fn main() {
+    cce_ui::engine::run::<ColorApp>();
 }