git.lucas.co / cce-layout-interface
page layout tool
git clone https://git.lucas.co/cce-layout-interface.git

commitde9ac8d0e2d00ac76e57bd19c3cbed088f58a6f7
authorLucas Galante <[email protected]>
date2026-06-03 10:14
Initial basic implementation of word processor + drawing layout interface

 .gitignore  |   2 +
 Cargo.toml  |  20 ++
 src/main.rs | 850 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 872 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..95e1c19
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "clear-layout-interface"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clear-ui = { path = "../clear-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"
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..1d22c60
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,850 @@
+use wayland_client::QueueHandle;
+use glyphon::{FontSystem, Buffer, Metrics, Attrs};
+use clear_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use clear_ui::widget::{
+    MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Widget,
+    MenuBar, TextBox, Slider, TextLabel
+};
+
+#[derive(Debug, Clone)]
+enum AppMessage {
+    Exit,
+    NewDocument,
+    AddText,
+    AddRectangle,
+    AddBanner,
+    ToggleGrid,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+enum ShapeType {
+    Rectangle,
+    Banner,
+}
+
+#[derive(Debug, Clone)]
+enum Element {
+    Text {
+        text: String,
+        x: f32,
+        y: f32,
+        w: f32,
+        h: f32,
+        font_size: f32,
+        color: [f32; 4],
+    },
+    Shape {
+        shape_type: ShapeType,
+        x: f32,
+        y: f32,
+        w: f32,
+        h: f32,
+        color: [f32; 4],
+    },
+}
+
+struct LayoutApp {
+    menu_bar: MenuBar,
+    elements: Vec<Element>,
+    selected_idx: Option<usize>,
+    dragging: Option<(usize, f32, f32)>, // Index, offset_x, offset_y
+    grid_enabled: bool,
+    width: u32,
+    height: u32,
+    scale_factor: f64,
+    text_items: Vec<TextItem>,
+    font_system: FontSystem,
+    needs_rebuild: bool,
+
+    // Sidebar property controls
+    sidebar_x: TextBox,
+    sidebar_y: TextBox,
+    sidebar_w: TextBox,
+    sidebar_h: TextBox,
+    sidebar_text: TextBox,
+    sidebar_size: TextBox,
+    
+    // Sliders for RGB colors
+    slider_r: Slider,
+    slider_g: Slider,
+    slider_b: Slider,
+    
+    last_selected: Option<usize>,
+}
+
+impl LayoutApp {
+    fn rebuild_text_items(&mut self) {
+        self.text_items.clear();
+        
+        let mut labels = Vec::new();
+        
+        // 1. MenuBar text labels
+        labels.extend(self.menu_bar.text_labels());
+        
+        // 2. Sidebar text boxes
+        labels.extend(self.sidebar_x.text_labels());
+        labels.extend(self.sidebar_y.text_labels());
+        labels.extend(self.sidebar_w.text_labels());
+        labels.extend(self.sidebar_h.text_labels());
+        labels.extend(self.sidebar_text.text_labels());
+        labels.extend(self.sidebar_size.text_labels());
+        
+        // 3. Slider Labels
+        labels.push(TextLabel {
+            text: format!("Red Color: {:.2}", self.slider_r.value()),
+            x: 20.0,
+            y: 320.0,
+            font_size: 11.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+        labels.push(TextLabel {
+            text: format!("Green Color: {:.2}", self.slider_g.value()),
+            x: 20.0,
+            y: 360.0,
+            font_size: 11.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+        labels.push(TextLabel {
+            text: format!("Blue Color: {:.2}", self.slider_b.value()),
+            x: 20.0,
+            y: 400.0,
+            font_size: 11.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+        
+        // 4. Section headers
+        labels.push(TextLabel {
+            text: "LAYOUT PROPERTIES".to_string(),
+            x: 20.0,
+            y: 42.0,
+            font_size: 13.0,
+            color: [0xee, 0xee, 0xf5],
+        });
+        
+        // Selection feedback
+        if let Some(idx) = self.selected_idx {
+            labels.push(TextLabel {
+                text: format!("Selected Element #{}", idx + 1),
+                x: 20.0,
+                y: 450.0,
+                font_size: 12.0,
+                color: [0x3b, 0x82, 0xf6],
+            });
+        } else {
+            labels.push(TextLabel {
+                text: "No Selection".to_string(),
+                x: 20.0,
+                y: 450.0,
+                font_size: 12.0,
+                color: [0x83, 0x83, 0x8a],
+            });
+            labels.push(TextLabel {
+                text: "Click canvas elements to edit properties.".to_string(),
+                x: 20.0,
+                y: 472.0,
+                font_size: 11.0,
+                color: [0x60, 0x60, 0x68],
+            });
+        }
+
+        // Help Instructions at the bottom of the sidebar
+        labels.push(TextLabel {
+            text: "INSTRUCTIONS:".to_string(),
+            x: 20.0,
+            y: 530.0,
+            font_size: 11.0,
+            color: [0xaa, 0xaa, 0xbb],
+        });
+        labels.push(TextLabel {
+            text: "- Click & Drag canvas elements to move".to_string(),
+            x: 20.0,
+            y: 550.0,
+            font_size: 10.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+        labels.push(TextLabel {
+            text: "- Use Menu Bar to add elements".to_string(),
+            x: 20.0,
+            y: 568.0,
+            font_size: 10.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+        labels.push(TextLabel {
+            text: "- Edit fields or drag Sliders to customize".to_string(),
+            x: 20.0,
+            y: 586.0,
+            font_size: 10.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+
+        // 5. Canvas Element Labels
+        for (idx, element) in self.elements.iter().enumerate() {
+            match element {
+                Element::Text { text, x, y, w: _, h: _, font_size, color } => {
+                    labels.push(TextLabel {
+                        text: text.clone(),
+                        x: *x + 8.0,
+                        y: *y + 6.0,
+                        font_size: *font_size,
+                        color: [
+                            (color[0] * 255.0).clamp(0.0, 255.0) as u8,
+                            (color[1] * 255.0).clamp(0.0, 255.0) as u8,
+                            (color[2] * 255.0).clamp(0.0, 255.0) as u8,
+                        ],
+                    });
+                }
+                Element::Shape { shape_type, x, y, w: _, h: _, color: _ } => {
+                    let type_str = match shape_type {
+                        ShapeType::Rectangle => "Rectangle",
+                        ShapeType::Banner => "Banner",
+                    };
+                    labels.push(TextLabel {
+                        text: format!("{} #{}", type_str, idx + 1),
+                        x: *x + 8.0,
+                        y: *y + 6.0,
+                        font_size: 11.0,
+                        color: [0xee, 0xee, 0xf5],
+                    });
+                }
+            }
+        }
+        
+        for label in labels {
+            let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
+            let mut buf = Buffer::new(&mut self.font_system, metrics);
+            buf.set_text(&mut self.font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
+            buf.shape_until_scroll(&mut self.font_system, true);
+            self.text_items.push(TextItem {
+                buffer: buf,
+                x: label.x,
+                y: label.y,
+                color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+            });
+        }
+    }
+
+    fn sync_sidebar_fields(&mut self) {
+        if let Some(idx) = self.selected_idx {
+            if self.last_selected != Some(idx) {
+                // Focus reset on selection swap
+                self.sidebar_text.unfocus();
+                self.sidebar_x.unfocus();
+                self.sidebar_y.unfocus();
+                self.sidebar_w.unfocus();
+                self.sidebar_h.unfocus();
+                self.sidebar_size.unfocus();
+                self.last_selected = Some(idx);
+            }
+            
+            let element = &self.elements[idx];
+            match element {
+                Element::Text { text, x, y, w, h, font_size, color } => {
+                    if !self.sidebar_text.editing { self.sidebar_text.text = text.clone(); }
+                    if !self.sidebar_x.editing { self.sidebar_x.text = format!("{:.0}", x); }
+                    if !self.sidebar_y.editing { self.sidebar_y.text = format!("{:.0}", y); }
+                    if !self.sidebar_w.editing { self.sidebar_w.text = format!("{:.0}", w); }
+                    if !self.sidebar_h.editing { self.sidebar_h.text = format!("{:.0}", h); }
+                    if !self.sidebar_size.editing { self.sidebar_size.text = format!("{:.0}", font_size); }
+                    
+                    self.slider_r.set_value(color[0]);
+                    self.slider_g.set_value(color[1]);
+                    self.slider_b.set_value(color[2]);
+                }
+                Element::Shape { shape_type: _, x, y, w, h, color } => {
+                    if !self.sidebar_text.editing { self.sidebar_text.text = "Shape Mode".to_string(); }
+                    if !self.sidebar_x.editing { self.sidebar_x.text = format!("{:.0}", x); }
+                    if !self.sidebar_y.editing { self.sidebar_y.text = format!("{:.0}", y); }
+                    if !self.sidebar_w.editing { self.sidebar_w.text = format!("{:.0}", w); }
+                    if !self.sidebar_h.editing { self.sidebar_h.text = format!("{:.0}", h); }
+                    if !self.sidebar_size.editing { self.sidebar_size.text = "N/A".to_string(); }
+                    
+                    self.slider_r.set_value(color[0]);
+                    self.slider_g.set_value(color[1]);
+                    self.slider_b.set_value(color[2]);
+                }
+            }
+        }
+    }
+
+    fn apply_sidebar_changes(&mut self) {
+        if let Some(idx) = self.selected_idx {
+            let text_val = if self.sidebar_text.editing { self.sidebar_text.edit_buffer.clone() } else { self.sidebar_text.text.clone() };
+            let x_val = if self.sidebar_x.editing { &self.sidebar_x.edit_buffer } else { &self.sidebar_x.text }.parse::<f32>().unwrap_or(0.0);
+            let y_val = if self.sidebar_y.editing { &self.sidebar_y.edit_buffer } else { &self.sidebar_y.text }.parse::<f32>().unwrap_or(0.0);
+            let w_val = if self.sidebar_w.editing { &self.sidebar_w.edit_buffer } else { &self.sidebar_w.text }.parse::<f32>().unwrap_or(100.0);
+            let h_val = if self.sidebar_h.editing { &self.sidebar_h.edit_buffer } else { &self.sidebar_h.text }.parse::<f32>().unwrap_or(50.0);
+            let size_val = if self.sidebar_size.editing { &self.sidebar_size.edit_buffer } else { &self.sidebar_size.text }.parse::<f32>().unwrap_or(16.0);
+            
+            let r_val = self.slider_r.value();
+            let g_val = self.slider_g.value();
+            let b_val = self.slider_b.value();
+            
+            match &mut self.elements[idx] {
+                Element::Text { text, x, y, w, h, font_size, color } => {
+                    *text = text_val;
+                    *x = x_val;
+                    *y = y_val;
+                    *w = w_val;
+                    *h = h_val;
+                    *font_size = size_val;
+                    *color = [r_val, g_val, b_val, 1.0];
+                }
+                Element::Shape { shape_type: _, x, y, w, h, color } => {
+                    *x = x_val;
+                    *y = y_val;
+                    *w = w_val;
+                    *h = h_val;
+                    *color = [r_val, g_val, b_val, 1.0];
+                }
+            }
+        }
+    }
+}
+
+impl Application for LayoutApp {
+    type Message = AppMessage;
+
+    fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
+        // Build the main MenuBar
+        let menu_bar = MenuBar::new(0.0, 0.0, 1024.0, 26.0)
+            .with_title("clear-layout")
+            .with_item("File", &["New Document", "Exit"])
+            .with_item("Insert", &["Add Text Box", "Add Rectangle", "Add Banner"])
+            .with_item("View", &["Toggle Grid"]);
+
+        // Initialize elements with a basic greeting layout
+        let elements = vec![
+            Element::Text {
+                text: "Welcome to Clear Layout!".to_string(),
+                x: 320.0,
+                y: 80.0,
+                w: 400.0,
+                h: 40.0,
+                font_size: 20.0,
+                color: [0.3, 0.7, 1.0, 1.0],
+            },
+            Element::Shape {
+                shape_type: ShapeType::Rectangle,
+                x: 320.0,
+                y: 130.0,
+                w: 600.0,
+                h: 4.0,
+                color: [0.2, 0.5, 0.9, 1.0],
+            },
+            Element::Text {
+                text: "This is a word processor and layout editor layout.".to_string(),
+                x: 320.0,
+                y: 160.0,
+                w: 500.0,
+                h: 30.0,
+                font_size: 14.0,
+                color: [0.8, 0.8, 0.85, 1.0],
+            },
+            Element::Shape {
+                shape_type: ShapeType::Rectangle,
+                x: 320.0,
+                y: 220.0,
+                w: 120.0,
+                h: 80.0,
+                color: [0.15, 0.25, 0.4, 1.0],
+            },
+            Element::Text {
+                text: "Card Label".to_string(),
+                x: 330.0,
+                y: 235.0,
+                w: 100.0,
+                h: 20.0,
+                font_size: 11.0,
+                color: [0.9, 0.9, 0.9, 1.0],
+            },
+        ];
+
+        // Create sidebar textbox inputs
+        let sidebar_x = TextBox::new(String::new()).with_label("X Position");
+        let sidebar_y = TextBox::new(String::new()).with_label("Y Position");
+        let sidebar_w = TextBox::new(String::new()).with_label("Width");
+        let sidebar_h = TextBox::new(String::new()).with_label("Height");
+        let sidebar_text = TextBox::new(String::new()).with_label("Text Content");
+        let sidebar_size = TextBox::new(String::new()).with_label("Text Size");
+
+        // Create color sliders
+        let slider_r = Slider::new().with_range(0.0, 1.0).with_value(0.5);
+        let slider_g = Slider::new().with_range(0.0, 1.0).with_value(0.5);
+        let slider_b = Slider::new().with_range(0.0, 1.0).with_value(0.5);
+
+        let mut app = Self {
+            menu_bar,
+            elements,
+            selected_idx: None,
+            dragging: None,
+            grid_enabled: true,
+            width: 1024,
+            height: 768,
+            scale_factor: 1.0,
+            text_items: Vec::new(),
+            font_system: FontSystem::new(),
+            needs_rebuild: true,
+            
+            sidebar_x,
+            sidebar_y,
+            sidebar_w,
+            sidebar_h,
+            sidebar_text,
+            sidebar_size,
+            
+            slider_r,
+            slider_g,
+            slider_b,
+            
+            last_selected: None,
+        };
+
+        app.sync_sidebar_fields();
+        app.rebuild_text_items();
+        app
+    }
+
+    fn settings(&self) -> WindowSettings {
+        WindowSettings {
+            title: "Clear Layout Interface".to_string(),
+            app_id: "clear-layout-interface".to_string(),
+            width: 1024,
+            height: 768,
+            fullscreen: false,
+            min_size: Some((800, 600)),
+        }
+    }
+
+    fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool) {
+        match msg {
+            AppMessage::Exit => {
+                *exit = true;
+            }
+            AppMessage::NewDocument => {
+                self.elements.clear();
+                self.selected_idx = None;
+                self.dragging = None;
+                self.sync_sidebar_fields();
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::AddText => {
+                let next_idx = self.elements.len();
+                self.elements.push(Element::Text {
+                    text: "New Text Block".to_string(),
+                    x: 350.0,
+                    y: 200.0,
+                    w: 200.0,
+                    h: 30.0,
+                    font_size: 14.0,
+                    color: [1.0, 1.0, 1.0, 1.0],
+                });
+                self.selected_idx = Some(next_idx);
+                self.sync_sidebar_fields();
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::AddRectangle => {
+                let next_idx = self.elements.len();
+                self.elements.push(Element::Shape {
+                    shape_type: ShapeType::Rectangle,
+                    x: 350.0,
+                    y: 200.0,
+                    w: 150.0,
+                    h: 100.0,
+                    color: [0.2, 0.6, 0.4, 1.0],
+                });
+                self.selected_idx = Some(next_idx);
+                self.sync_sidebar_fields();
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::AddBanner => {
+                let next_idx = self.elements.len();
+                self.elements.push(Element::Shape {
+                    shape_type: ShapeType::Banner,
+                    x: 350.0,
+                    y: 200.0,
+                    w: 300.0,
+                    h: 50.0,
+                    color: [0.8, 0.4, 0.2, 1.0],
+                });
+                self.selected_idx = Some(next_idx);
+                self.sync_sidebar_fields();
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::ToggleGrid => {
+                self.grid_enabled = !self.grid_enabled;
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
+    }
+
+    fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
+
+    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
+        let size_changed = self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale;
+        if self.needs_rebuild || size_changed {
+            self.width = size.width as u32;
+            self.height = size.height as u32;
+            self.scale_factor = scale;
+            
+            // Layout MenuBar at the top
+            self.menu_bar.set_rect(0.0, 0.0, size.width, 26.0);
+            
+            // Position sidebar controls
+            self.sidebar_x.set_rect(20.0, 70.0, 110.0, 26.0);
+            self.sidebar_y.set_rect(150.0, 70.0, 110.0, 26.0);
+            self.sidebar_w.set_rect(20.0, 130.0, 110.0, 26.0);
+            self.sidebar_h.set_rect(150.0, 130.0, 110.0, 26.0);
+            self.sidebar_text.set_rect(20.0, 190.0, 240.0, 26.0);
+            self.sidebar_size.set_rect(20.0, 250.0, 240.0, 26.0);
+            
+            self.slider_r.set_rect(20.0, 330.0, 240.0, 18.0);
+            self.slider_g.set_rect(20.0, 370.0, 240.0, 18.0);
+            self.slider_b.set_rect(20.0, 410.0, 240.0, 18.0);
+
+            self.rebuild_text_items();
+            self.needs_rebuild = false;
+        }
+
+        // 1. Sidebar Background Panel
+        quads.push((0.0, 26.0, 280.0, self.height as f32 - 26.0, [0.08, 0.08, 0.10, 1.0]));
+
+        // 2. Canvas Background
+        quads.push((280.0, 26.0, self.width as f32 - 280.0, self.height as f32 - 26.0, [0.12, 0.12, 0.15, 1.0]));
+
+        // Divider between Sidebar and Canvas
+        quads.push((280.0, 26.0, 1.0, self.height as f32 - 26.0, [0.20, 0.20, 0.25, 1.0]));
+
+        // 3. Grid rendering
+        if self.grid_enabled {
+            let spacing = 20.0;
+            let dot_color = [0.25, 0.25, 0.32, 0.35];
+            let mut cx = 280.0 + spacing;
+            while cx < self.width as f32 {
+                let mut cy = 26.0 + spacing;
+                while cy < self.height as f32 {
+                    quads.push((cx, cy, 1.5, 1.5, dot_color));
+                    cy += spacing;
+                }
+                cx += spacing;
+            }
+        }
+
+        // 4. Render Layout Elements
+        for (idx, element) in self.elements.iter().enumerate() {
+            match element {
+                Element::Text { text: _, x, y, w, h, font_size: _, color: _ } => {
+                    // Translucent backing for text boundary
+                    quads.push((*x, *y, *w, *h, [0.18, 0.18, 0.22, 0.25]));
+                    // Subtle text box border
+                    let border_color = [0.25, 0.25, 0.30, 0.5];
+                    quads.push((*x, *y, *w, 1.0, border_color));
+                    quads.push((*x, *y + *h - 1.0, *w, 1.0, border_color));
+                    quads.push((*x, *y, 1.0, *h, border_color));
+                    quads.push((*x + *w - 1.0, *y, 1.0, *h, border_color));
+                }
+                Element::Shape { shape_type, x, y, w, h, color } => {
+                    match shape_type {
+                        ShapeType::Rectangle => {
+                            quads.push((*x, *y, *w, *h, *color));
+                        }
+                        ShapeType::Banner => {
+                            // Fancy drop shadow
+                            quads.push((*x + 3.0, *y + 3.0, *w, *h, [0.05, 0.05, 0.08, 0.4]));
+                            // Main banner quad
+                            quads.push((*x, *y, *w, *h, *color));
+                            // Inner outline
+                            let inner_col = [1.0, 1.0, 1.0, 0.2];
+                            quads.push((*x + 2.0, *y + 2.0, *w - 4.0, 1.0, inner_col));
+                            quads.push((*x + 2.0, *y + *h - 3.0, *w - 4.0, 1.0, inner_col));
+                            quads.push((*x + 2.0, *y + 2.0, 1.0, *h - 4.0, inner_col));
+                            quads.push((*x + *w - 3.0, *y + 2.0, 1.0, *h - 4.0, inner_col));
+                        }
+                    }
+                }
+            }
+
+            // Draw highlight border if selected
+            if self.selected_idx == Some(idx) {
+                let (ex, ey, ew, eh) = match element {
+                    Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
+                    Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
+                };
+                let sel_col = [0.23, 0.51, 0.96, 1.0]; // Bright neon blue
+                quads.push((ex - 1.0, ey - 1.0, ew + 2.0, 1.0, sel_col));
+                quads.push((ex - 1.0, ey + eh, ew + 2.0, 1.0, sel_col));
+                quads.push((ex - 1.0, ey - 1.0, 1.0, eh + 2.0, sel_col));
+                quads.push((ex + ew, ey - 1.0, 1.0, eh + 2.0, sel_col));
+
+                // Corner handles
+                let hs = 5.0;
+                let hc = [1.0, 1.0, 1.0, 1.0];
+                let hborder = [0.23, 0.51, 0.96, 1.0];
+                let handles = [
+                    (ex - 2.0, ey - 2.0),
+                    (ex + ew - 3.0, ey - 2.0),
+                    (ex - 2.0, ey + eh - 3.0),
+                    (ex + ew - 3.0, ey + eh - 3.0),
+                ];
+                for (hx, hy) in handles {
+                    quads.push((hx, hy, hs, hs, hborder));
+                    quads.push((hx + 1.0, hy + 1.0, hs - 2.0, hs - 2.0, hc));
+                }
+            }
+        }
+
+        // 5. Sidebar Widget extra quads (backgrounds, highlights, values)
+        quads.extend(self.sidebar_x.extra_quads());
+        quads.extend(self.sidebar_y.extra_quads());
+        quads.extend(self.sidebar_w.extra_quads());
+        quads.extend(self.sidebar_h.extra_quads());
+        quads.extend(self.sidebar_text.extra_quads());
+        quads.extend(self.sidebar_size.extra_quads());
+
+        let (rx, ry, rw, rh) = self.slider_r.rect();
+        quads.push((rx, ry, rw, rh, self.slider_r.color()));
+        quads.extend(self.slider_r.extra_quads());
+
+        let (gx, gy, gw, gh) = self.slider_g.rect();
+        quads.push((gx, gy, gw, gh, self.slider_g.color()));
+        quads.extend(self.slider_g.extra_quads());
+
+        let (bx, by, bw, bh) = self.slider_b.rect();
+        quads.push((bx, by, bw, bh, self.slider_b.color()));
+        quads.extend(self.slider_b.extra_quads());
+
+        // 6. MenuBar Background
+        let mb_color = self.menu_bar.color();
+        let (mb_x, mb_y, mb_w, mb_h) = self.menu_bar.rect();
+        quads.push((mb_x, mb_y, mb_w, mb_h, mb_color));
+
+        // Header Highlights for Open Menus
+        for menu in &self.menu_bar.menus {
+            if let Some(hq) = menu.highlight_quad() {
+                quads.push(hq);
+            }
+        }
+        quads.extend(self.menu_bar.extra_quads());
+    }
+
+    fn text_items(&self) -> &[TextItem] {
+        &self.text_items
+    }
+
+    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        let mut changed = false;
+        let px = pos.x as f32;
+        let py = pos.y as f32;
+
+        if self.menu_bar.is_menu_open() || self.menu_bar.hit_test(px, py) {
+            if self.menu_bar.on_cursor_moved(px, py) {
+                changed = true;
+            }
+        } else {
+            let sidebar_w = 280.0;
+            if px < sidebar_w {
+                if self.sidebar_x.on_cursor_moved(px, py) { changed = true; }
+                if self.sidebar_y.on_cursor_moved(px, py) { changed = true; }
+                if self.sidebar_w.on_cursor_moved(px, py) { changed = true; }
+                if self.sidebar_h.on_cursor_moved(px, py) { changed = true; }
+                if self.sidebar_text.on_cursor_moved(px, py) { changed = true; }
+                if self.sidebar_size.on_cursor_moved(px, py) { changed = true; }
+                
+                if self.slider_r.is_dragging() {
+                    if self.slider_r.drag_update(px, py) { changed = true; }
+                } else if self.slider_r.on_cursor_moved(px, py) { changed = true; }
+
+                if self.slider_g.is_dragging() {
+                    if self.slider_g.drag_update(px, py) { changed = true; }
+                } else if self.slider_g.on_cursor_moved(px, py) { changed = true; }
+
+                if self.slider_b.is_dragging() {
+                    if self.slider_b.drag_update(px, py) { changed = true; }
+                } else if self.slider_b.on_cursor_moved(px, py) { changed = true; }
+                
+                if changed {
+                    self.apply_sidebar_changes();
+                }
+            } else {
+                if let Some((idx, ox, oy)) = self.dragging {
+                    let mut new_x = px - ox;
+                    let mut new_y = py - oy;
+                    
+                    if self.grid_enabled {
+                        new_x = (new_x / 20.0).round() * 20.0;
+                        new_y = (new_y / 20.0).round() * 20.0;
+                    }
+                    
+                    match &mut self.elements[idx] {
+                        Element::Text { x, y, .. } => {
+                            *x = new_x;
+                            *y = new_y;
+                        }
+                        Element::Shape { x, y, .. } => {
+                            *x = new_x;
+                            *y = new_y;
+                        }
+                    }
+                    self.sync_sidebar_fields();
+                    changed = true;
+                } else {
+                    // Feed hovered states to buttons
+                    if self.sidebar_x.on_cursor_moved(px, py) { changed = true; }
+                    if self.sidebar_y.on_cursor_moved(px, py) { changed = true; }
+                    if self.sidebar_w.on_cursor_moved(px, py) { changed = true; }
+                    if self.sidebar_h.on_cursor_moved(px, py) { changed = true; }
+                    if self.sidebar_text.on_cursor_moved(px, py) { changed = true; }
+                    if self.sidebar_size.on_cursor_moved(px, py) { changed = true; }
+                }
+            }
+            if self.menu_bar.on_cursor_moved(px, py) {
+                changed = true;
+            }
+        }
+
+        if changed {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+    }
+
+    fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        let mut changed = false;
+        let mut msg_out = None;
+        let px = pos.x as f32;
+        let py = pos.y as f32;
+
+        if self.menu_bar.is_menu_open() || self.menu_bar.hit_test(px, py) {
+            if self.menu_bar.mouse_input(button, state, px, py) {
+                changed = true;
+                if let Some((menu_idx, item_idx)) = self.menu_bar.menu_click() {
+                    match (menu_idx, item_idx) {
+                        (0, 0) => msg_out = Some(AppMessage::NewDocument),
+                        (0, 1) => msg_out = Some(AppMessage::Exit),
+                        (1, 0) => msg_out = Some(AppMessage::AddText),
+                        (1, 1) => msg_out = Some(AppMessage::AddRectangle),
+                        (1, 2) => msg_out = Some(AppMessage::AddBanner),
+                        (2, 0) => msg_out = Some(AppMessage::ToggleGrid),
+                        _ => {}
+                    }
+                }
+            }
+            if state == ElementState::Pressed && !self.menu_bar.hit_test(px, py) {
+                self.menu_bar.unfocus();
+                changed = true;
+            }
+        } else {
+            let sidebar_w = 280.0;
+            if px < sidebar_w {
+                if state == ElementState::Pressed {
+                    let mut clicked = false;
+                    if self.sidebar_x.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.sidebar_y.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.sidebar_w.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.sidebar_h.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.sidebar_text.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.sidebar_size.mouse_input(button, state, px, py) { clicked = true; }
+                    
+                    if self.slider_r.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.slider_g.mouse_input(button, state, px, py) { clicked = true; }
+                    if self.slider_b.mouse_input(button, state, px, py) { clicked = true; }
+
+                    if !self.sidebar_x.hit_test(px, py) { self.sidebar_x.unfocus(); }
+                    if !self.sidebar_y.hit_test(px, py) { self.sidebar_y.unfocus(); }
+                    if !self.sidebar_w.hit_test(px, py) { self.sidebar_w.unfocus(); }
+                    if !self.sidebar_h.hit_test(px, py) { self.sidebar_h.unfocus(); }
+                    if !self.sidebar_text.hit_test(px, py) { self.sidebar_text.unfocus(); }
+                    if !self.sidebar_size.hit_test(px, py) { self.sidebar_size.unfocus(); }
+
+                    if clicked {
+                        self.apply_sidebar_changes();
+                        changed = true;
+                    }
+                } else {
+                    self.slider_r.mouse_input(button, state, px, py);
+                    self.slider_g.mouse_input(button, state, px, py);
+                    self.slider_b.mouse_input(button, state, px, py);
+                    self.apply_sidebar_changes();
+                    changed = true;
+                }
+            } else {
+                if state == ElementState::Pressed {
+                    let mut found = None;
+                    for (idx, element) in self.elements.iter().enumerate().rev() {
+                        let (ex, ey, ew, eh) = match element {
+                            Element::Text { x, y, w, h, .. } => (*x, *y, *w, *h),
+                            Element::Shape { x, y, w, h, .. } => (*x, *y, *w, *h),
+                        };
+                        if px >= ex && px <= ex + ew && py >= ey && py <= ey + eh {
+                            found = Some(idx);
+                            break;
+                        }
+                    }
+
+                    if let Some(idx) = found {
+                        self.selected_idx = Some(idx);
+                        let (ex, ey) = match &self.elements[idx] {
+                            Element::Text { x, y, .. } => (*x, *y),
+                            Element::Shape { x, y, .. } => (*x, *y),
+                        };
+                        self.dragging = Some((idx, px - ex, py - ey));
+                        self.sync_sidebar_fields();
+                        changed = true;
+                    } else {
+                        self.selected_idx = None;
+                        self.dragging = None;
+                        changed = true;
+                    }
+                } else if state == ElementState::Released {
+                    self.dragging = None;
+                }
+            }
+        }
+
+        if changed {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+
+        msg_out
+    }
+
+    fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
+
+    fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        let mut handled = false;
+        
+        if self.sidebar_x.editing {
+            if self.sidebar_x.keyboard_input(event) { handled = true; }
+        } else if self.sidebar_y.editing {
+            if self.sidebar_y.keyboard_input(event) { handled = true; }
+        } else if self.sidebar_w.editing {
+            if self.sidebar_w.keyboard_input(event) { handled = true; }
+        } else if self.sidebar_h.editing {
+            if self.sidebar_h.keyboard_input(event) { handled = true; }
+        } else if self.sidebar_text.editing {
+            if self.sidebar_text.keyboard_input(event) { handled = true; }
+        } else if self.sidebar_size.editing {
+            if self.sidebar_size.keyboard_input(event) { handled = true; }
+        }
+
+        if handled {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+            self.apply_sidebar_changes();
+        }
+
+        None
+    }
+}
+
+fn main() {
+    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+    let _guard = rt.enter();
+    
+    clear_ui::engine::run::<LayoutApp>();
+}