git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit2bae918d7e19219e781b4ec6c5c8aa6f8f1674a7
parent2053c61bcb
authorLucas Galante <[email protected]>
date2026-05-26 21:18
Update system configuration and interface modules

 src/layout.rs |  212 +++++++++--
 src/main.rs   |  222 ++++++++---
 src/widget.rs | 1170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 1500 insertions(+), 104 deletions(-)

diff --git a/src/layout.rs b/src/layout.rs
index ed07b20..5bf2e89 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -3,6 +3,9 @@ use crate::widget::Widget;
 pub trait RenderTarget {
     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]);
+    fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _font: &str) {
+        self.text(content, x, y, size, color);
+    }
 }
 
 pub fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
@@ -14,19 +17,19 @@ pub fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y:
     if let Some(hc) = w.hover_highlight() {
         pc.rect(hc, x, y, ww, wh);
     }
+    let font_opt = w.widget_font();
     for label in w.text_labels() {
-        pc.text(
-            &label.text,
-            label.x,
-            label.y,
-            label.font_size,
-            [
-                label.color[0] as f32 / 255.0,
-                label.color[1] as f32 / 255.0,
-                label.color[2] as f32 / 255.0,
-                1.0,
-            ],
-        );
+        let color_f32 = [
+            label.color[0] as f32 / 255.0,
+            label.color[1] as f32 / 255.0,
+            label.color[2] as f32 / 255.0,
+            1.0,
+        ];
+        if let Some(ref font) = font_opt {
+            pc.text_with_font(&label.text, label.x, label.y, label.font_size, color_f32, font);
+        } else {
+            pc.text(&label.text, label.x, label.y, label.font_size, color_f32);
+        }
     }
 }
 
@@ -98,6 +101,8 @@ impl Column {
             pc: &mut *pc,
             base_x: self.ox + self.cx,
             y: row_y,
+            cursor_x: 0.0,
+            spacing: 8.0,
         };
         f(&mut row);
         self.y = self.y + h;
@@ -108,16 +113,28 @@ pub struct Row<'a> {
     pc: &'a mut dyn RenderTarget,
     base_x: f32,
     y: f32,
+    pub cursor_x: f32,
+    pub spacing: f32,
 }
 
 impl<'a> Row<'a> {
-    pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+    pub fn set_spacing(&mut self, spacing: f32) {
+        self.spacing = spacing;
+    }
+
+    pub fn gap(&mut self, width: f32) {
+        self.cursor_x += width;
+    }
+
+    pub fn text(&mut self, text: &str, y_off: f32, font_size: f32, color: [f32; 4], width: f32) {
         self.pc
-            .text(text, self.base_x + x_off, self.y + y_off, font_size, color);
+            .text(text, self.base_x + self.cursor_x, self.y + y_off, font_size, color);
+        self.cursor_x += width + self.spacing;
     }
 
-    pub fn widget<T: Widget>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
-        render_widget(self.pc, w, self.base_x + x_off, self.y, ww, wh);
+    pub fn widget<T: Widget>(&mut self, w: &mut T, ww: f32, wh: f32) {
+        render_widget(self.pc, w, self.base_x + self.cursor_x, self.y, ww, wh);
+        self.cursor_x += ww + self.spacing;
     }
 }
 
@@ -129,13 +146,20 @@ pub struct Section {
 }
 
 impl Section {
+    pub const ROW_PADDING_X: f32 = 8.0;
+    pub const DEFAULT_MARGIN_X: f32 = 12.0;
+    pub const DEFAULT_ROW_GAP: f32 = 8.0;
+
     pub fn new(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
-        pc.text(label, left + 12.0, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
-        pc.rect([0.18, 0.18, 0.27, 1.0], left + 8.0, top + 22.0, cw - 16.0, 1.0);
-        Self { left, top, content_y: top + 40.0, cw }
+        pc.text(label, left + Self::DEFAULT_MARGIN_X, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
+        pc.rect([0.18, 0.18, 0.27, 1.0], left + Self::ROW_PADDING_X, top + 22.0, cw - 2.0 * Self::ROW_PADDING_X, 1.0);
+        Self { left, top, content_y: top + 34.0, cw }
     }
 
-    pub fn ax(&self, x_off: f32) -> f32 { self.left + x_off }
+    pub fn ax(&self, x_off: f32) -> f32 {
+        let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
+        self.left + x_off + shift
+    }
 
     pub fn ay(&self) -> f32 { self.content_y }
 
@@ -146,15 +170,17 @@ impl Section {
     }
 
     pub fn widget<T: Widget>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
-        w.set_row_rect(self.left + 8.0, self.cw - 16.0);
+        w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+        let top_room = w.top_room();
+        self.content_y += top_room;
         render_widget(pc, w, self.ax(x_off), self.ay(), ww, wh);
-        self.content_y += wh + w.top_room();
+        self.content_y += wh;
     }
 
     pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
-        let x = self.ax(8.0);
+        let x = self.ax(Self::ROW_PADDING_X);
         let y = self.ay();
-        pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
+        pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
         self.content_y += 8.0;
     }
 
@@ -164,7 +190,8 @@ impl Section {
     }
 
     pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
-        let usable_w = self.cw - 24.0; // 12.0 padding on left and right inside outer bounds
+        let margin_x = Self::ROW_PADDING_X + 12.0; // 20.0 px (12.0 px inner padding)
+        let usable_w = self.cw - 2.0 * margin_x; // padding on left and right inside outer bounds
         if count == 0 {
             return Vec::new();
         }
@@ -173,7 +200,7 @@ impl Section {
 
         let mut cols = Vec::with_capacity(count);
         for i in 0..count {
-            let x = self.left + 12.0 + i as f32 * (col_w + gap);
+            let x = self.left + margin_x + i as f32 * (col_w + gap);
             cols.push((x, col_w));
         }
         cols
@@ -200,14 +227,135 @@ impl Section {
         } else {
             [0.25, 0.25, 0.35, 1.0] // Default gray
         };
-        let x = self.left + 8.0;
+        let x = self.left + Self::ROW_PADDING_X;
         let y = self.top + 22.0;
-        let w = self.cw - 16.0;
+        let w = self.cw - 2.0 * Self::ROW_PADDING_X;
         let h = self.content_y - y;
         pc.rect(border, x, y, w, 1.0);
-        pc.rect(border, x, y + h + 4.0, w, 1.0);
-        pc.rect(border, x, y, 1.0, h + 4.0);
-        pc.rect(border, x + w - 1.0, y, 1.0, h + 4.0);
-        self.content_y + 12.0
+        pc.rect(border, x, y + h + 12.0, w, 1.0);
+        pc.rect(border, x, y, 1.0, h + 12.0);
+        pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
+        self.content_y + 20.0
+    }
+
+    pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> VStack<'a> {
+        VStack {
+            section: self,
+            pc,
+            spacing,
+        }
+    }
+}
+
+pub struct VStack<'a> {
+    section: &'a mut Section,
+    pc: &'a mut dyn RenderTarget,
+    spacing: f32,
+}
+
+impl<'a> VStack<'a> {
+    pub fn add_widget<T: Widget>(&mut self, w: &mut T, ww: f32, wh: f32) {
+        self.section.widget(self.pc, w, Section::DEFAULT_MARGIN_X, ww, wh);
+        self.section.spacing(self.spacing);
+    }
+
+    pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, f: F)
+    where
+        F: FnMut(usize, f32, f32),
+    {
+        self.section.row(count, gap, h, f);
+        self.section.spacing(self.spacing);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    struct MockRenderTarget {
+        rects: Vec<([f32; 4], f32, f32, f32, f32)>,
+    }
+
+    impl RenderTarget for MockRenderTarget {
+        fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
+            self.rects.push((color, x, y, w, h));
+        }
+        fn text(&mut self, _content: &str, _x: f32, _y: f32, _size: f32, _color: [f32; 4]) {}
+    }
+
+    struct MockWidget {
+        x: f32,
+        y: f32,
+        w: f32,
+        h: f32,
+    }
+
+    impl Widget for MockWidget {
+        fn rect(&self) -> (f32, f32, f32, f32) {
+            (self.x, self.y, self.w, self.h)
+        }
+        fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+            self.x = x;
+            self.y = y;
+            self.w = w;
+            self.h = h;
+        }
+        fn color(&self) -> [f32; 4] {
+            [0.0, 0.0, 0.0, 0.0]
+        }
+    }
+
+    struct MockWidgetWithLabel {
+        x: f32,
+        y: f32,
+        w: f32,
+        h: f32,
+        top_room: f32,
+    }
+
+    impl Widget for MockWidgetWithLabel {
+        fn rect(&self) -> (f32, f32, f32, f32) {
+            (self.x, self.y, self.w, self.h)
+        }
+        fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+            self.x = x;
+            self.y = y;
+            self.w = w;
+            self.h = h;
+        }
+        fn color(&self) -> [f32; 4] {
+            [0.0, 0.0, 0.0, 0.0]
+        }
+        fn top_room(&self) -> f32 {
+            self.top_room
+        }
+    }
+
+    #[test]
+    fn test_vstack_flow() {
+        let mut mock_pc = MockRenderTarget { rects: Vec::new() };
+        let mut sec = Section::new(&mut mock_pc, 10.0, 20.0, 200.0, "Test Section");
+        
+        let start_y = sec.ay();
+        let mut stack = sec.vstack(&mut mock_pc, 10.0);
+
+        let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+        stack.add_widget(&mut w1, 50.0, 30.0);
+
+        // Standard margin should be applied
+        assert_eq!(w1.x, 30.0);
+        assert_eq!(w1.y, start_y);
+
+        let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+        stack.add_widget(&mut w2, 60.0, 40.0);
+
+        // Second widget should start after first widget height + vstack spacing
+        assert_eq!(w2.y, start_y + 30.0 + 10.0);
+
+        let mut w3 = MockWidgetWithLabel { x: 0.0, y: 0.0, w: 0.0, h: 0.0, top_room: 15.0 };
+        stack.add_widget(&mut w3, 70.0, 50.0);
+
+        // Third widget has top_room = 15.0, so its y should be shifted by 15.0
+        assert_eq!(w3.y, start_y + 30.0 + 10.0 + 40.0 + 10.0 + 15.0);
     }
 }
diff --git a/src/main.rs b/src/main.rs
index ab280c9..6af9574 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -317,6 +317,11 @@ impl State {
                 verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
             }
         }
+        if clear_ui::widget::context_menu::is_visible() {
+            for (qx, qy, qw, qh, qc) in clear_ui::widget::context_menu::extra_quads() {
+                verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+            }
+        }
         verts
     }
 
@@ -405,6 +410,13 @@ impl State {
             }
         }
 
+        if clear_ui::widget::context_menu::is_visible() {
+            for label in clear_ui::widget::context_menu::text_labels() {
+                widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+                widget_labels.push(label);
+            }
+        }
+
         for (buf, label) in widget_buffers.iter().zip(widget_labels.iter()) {
             areas.push(TextArea {
                 buffer: buf,
@@ -521,6 +533,29 @@ fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
     ]
 }
 
+struct PressedKey {
+    logical_key: clear_ui::widget::Key,
+    text: Option<String>,
+    first_pressed: std::time::Instant,
+    last_repeated: std::time::Instant,
+}
+
+fn is_repeatable_key(key: &clear_ui::widget::Key) -> bool {
+    use clear_ui::widget::{Key, NamedKey};
+    match key {
+        Key::Named(NamedKey::Backspace) |
+        Key::Named(NamedKey::Delete) |
+        Key::Named(NamedKey::ArrowLeft) |
+        Key::Named(NamedKey::ArrowRight) |
+        Key::Named(NamedKey::ArrowUp) |
+        Key::Named(NamedKey::ArrowDown) |
+        Key::Named(NamedKey::Home) |
+        Key::Named(NamedKey::End) |
+        Key::Character(_) => true,
+        _ => false,
+    }
+}
+
 struct AppState {
     registry_state: RegistryState,
     compositor_state: CompositorState,
@@ -540,6 +575,8 @@ struct AppState {
     exit: bool,
     redraw: bool,
     ctrl_pressed: bool,
+    shift_pressed: bool,
+    pressed_key: Option<PressedKey>,
 }
 
 impl CompositorHandler for AppState {
@@ -683,17 +720,23 @@ impl PointerHandler for AppState {
                 PointerEventKind::Motion { .. } => {
                     if let Some(state) = &mut self.state {
                         let mut changed = false;
-                        if let Some(idx) = state.drag_widget {
-                            if state.widgets[idx].drag_update(state.cursor_x, state.cursor_y) {
+                        if clear_ui::widget::context_menu::is_visible() {
+                            if clear_ui::widget::context_menu::cursor_moved(state.cursor_x, state.cursor_y) {
                                 changed = true;
                             }
-                        }
-                        if state.drag_widget.is_none() {
-                            for w in &mut state.widgets {
-                                if w.cursor_moved(state.cursor_x, state.cursor_y) {
+                        } else {
+                            if let Some(idx) = state.drag_widget {
+                                if state.widgets[idx].drag_update(state.cursor_x, state.cursor_y) {
                                     changed = true;
                                 }
                             }
+                            if state.drag_widget.is_none() {
+                                for w in &mut state.widgets {
+                                    if w.cursor_moved(state.cursor_x, state.cursor_y) {
+                                        changed = true;
+                                    }
+                                }
+                            }
                         }
                         if changed {
                             state.upload_vertices();
@@ -702,33 +745,44 @@ impl PointerHandler for AppState {
                     }
                 }
                 PointerEventKind::Press { button, .. } => {
-                    if *button != 272 {
-                        continue;
-                    }
+                    let btn = match *button {
+                        272 => clear_ui::widget::MouseButton::Left,
+                        273 => clear_ui::widget::MouseButton::Right,
+                        274 => clear_ui::widget::MouseButton::Middle,
+                        _ => continue,
+                    };
                     if let Some(st) = &mut self.state {
-                        let button = clear_ui::widget::MouseButton::Left;
-                        let btn_state = clear_ui::widget::ElementState::Pressed;
                         let mut changed = false;
-                        if let Some(old) = st.focused_widget.take() {
-                            st.widgets[old].unfocus();
-                        }
-                        for i in (0..st.widgets.len()).rev() {
-                            if st.widgets[i].hit_test(st.cursor_x, st.cursor_y) {
-                                if st.widgets[i].mouse_input(
-                                    button,
-                                    btn_state,
-                                    st.cursor_x,
-                                    st.cursor_y,
-                                ) {
-                                    changed = true;
+                        if clear_ui::widget::context_menu::is_visible() {
+                            if clear_ui::widget::context_menu::mouse_input(btn, clear_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y) {
+                                changed = true;
+                            }
+                        } else {
+                            if btn == clear_ui::widget::MouseButton::Left {
+                                if let Some(old) = st.focused_widget.take() {
+                                    st.widgets[old].unfocus();
                                 }
-                                if st.widgets[i].draggable() {
-                                    st.widgets[i].drag_begin(st.cursor_x, st.cursor_y);
-                                    st.drag_widget = Some(i);
+                            }
+                            for i in (0..st.widgets.len()).rev() {
+                                if st.widgets[i].hit_test(st.cursor_x, st.cursor_y) {
+                                    if st.widgets[i].mouse_input(
+                                        btn,
+                                        clear_ui::widget::ElementState::Pressed,
+                                        st.cursor_x,
+                                        st.cursor_y,
+                                    ) {
+                                        changed = true;
+                                    }
+                                    if btn == clear_ui::widget::MouseButton::Left && st.widgets[i].draggable() {
+                                        st.widgets[i].drag_begin(st.cursor_x, st.cursor_y);
+                                        st.drag_widget = Some(i);
+                                    }
+                                    if btn == clear_ui::widget::MouseButton::Left {
+                                        st.widgets[i].focus();
+                                        st.focused_widget = Some(i);
+                                    }
+                                    break;
                                 }
-                                st.widgets[i].focus();
-                                st.focused_widget = Some(i);
-                                break;
                             }
                         }
                         if changed {
@@ -738,32 +792,43 @@ impl PointerHandler for AppState {
                     }
                 }
                 PointerEventKind::Release { button, .. } => {
-                    if *button != 272 {
-                        continue;
-                    }
+                    let btn = match *button {
+                        272 => clear_ui::widget::MouseButton::Left,
+                        273 => clear_ui::widget::MouseButton::Right,
+                        274 => clear_ui::widget::MouseButton::Middle,
+                        _ => continue,
+                    };
                     if let Some(st) = &mut self.state {
-                        let button = clear_ui::widget::MouseButton::Left;
-                        let btn_state = clear_ui::widget::ElementState::Released;
                         let mut changed = false;
-                        if let Some(idx) = st.drag_widget {
-                            st.widgets[idx].drag_end();
-                            st.drag_widget = None;
-                            changed = true;
-                        }
-                        for w in &mut st.widgets {
-                            if w.mouse_input(button, btn_state, st.cursor_x, st.cursor_y) {
+                        if clear_ui::widget::context_menu::is_visible() {
+                            if clear_ui::widget::context_menu::mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
                                 changed = true;
                             }
-                        }
-                        let mut clicked = false;
-                        for w in &mut st.widgets {
-                            if w.take_click() {
-                                clicked = true;
+                        } else {
+                            if btn == clear_ui::widget::MouseButton::Left {
+                                if let Some(idx) = st.drag_widget {
+                                    st.widgets[idx].drag_end();
+                                    st.drag_widget = None;
+                                    changed = true;
+                                }
+                            }
+                            for w in &mut st.widgets {
+                                if w.mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
+                                    changed = true;
+                                }
+                            }
+                            if btn == clear_ui::widget::MouseButton::Left {
+                                let mut clicked = false;
+                                for w in &mut st.widgets {
+                                    if w.take_click() {
+                                        clicked = true;
+                                    }
+                                }
+                                if clicked {
+                                    st.click_count += 1;
+                                    st.update_status_text(&format!("Clicks: {}", st.click_count));
+                                }
                             }
-                        }
-                        if clicked {
-                            st.click_count += 1;
-                            st.update_status_text(&format!("Clicks: {}", st.click_count));
                         }
                         if changed {
                             st.upload_vertices();
@@ -849,6 +914,7 @@ impl KeyboardHandler for AppState {
         _layout: u32,
     ) {
         self.ctrl_pressed = modifiers.ctrl;
+        self.shift_pressed = modifiers.shift;
     }
 }
 
@@ -881,8 +947,28 @@ impl AppState {
             text: event.utf8.clone(),
             repeat: false,
             ctrl: self.ctrl_pressed,
+            shift: self.shift_pressed,
         };
 
+        if state == clear_ui::widget::ElementState::Pressed {
+            if is_repeatable_key(&custom_event.logical_key) {
+                self.pressed_key = Some(PressedKey {
+                    logical_key: custom_event.logical_key.clone(),
+                    text: custom_event.text.clone(),
+                    first_pressed: std::time::Instant::now(),
+                    last_repeated: std::time::Instant::now(),
+                });
+            } else {
+                self.pressed_key = None;
+            }
+        } else if state == clear_ui::widget::ElementState::Released {
+            if let Some(ref pk) = self.pressed_key {
+                if pk.logical_key == custom_event.logical_key {
+                    self.pressed_key = None;
+                }
+            }
+        }
+
         if let Some(st) = &mut self.state {
             if let Some(idx) = st.focused_widget {
                 let val = st.widgets[idx].value();
@@ -986,6 +1072,8 @@ fn main() {
         exit: false,
         redraw: true,
         ctrl_pressed: false,
+        shift_pressed: false,
+        pressed_key: None,
     };
 
     // Perform a roundtrip to populate output_state with active output scales
@@ -1020,6 +1108,9 @@ fn main() {
     let loop_handle = event_loop.handle();
     WaylandSource::new(conn, event_queue).insert(loop_handle).unwrap();
 
+    const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
+    const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
+
     loop {
         event_loop
             .dispatch(std::time::Duration::from_millis(16), &mut app)
@@ -1027,6 +1118,37 @@ fn main() {
         if app.exit {
             break;
         }
+
+        if let Some(ref mut pk) = app.pressed_key {
+            let now = std::time::Instant::now();
+            if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
+                if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
+                    pk.last_repeated = now;
+                    let custom_event = clear_ui::widget::KeyEvent {
+                        state: clear_ui::widget::ElementState::Pressed,
+                        logical_key: pk.logical_key.clone(),
+                        text: pk.text.clone(),
+                        repeat: true,
+                        ctrl: app.ctrl_pressed,
+                        shift: app.shift_pressed,
+                    };
+                    if let Some(st) = &mut app.state {
+                        if let Some(idx) = st.focused_widget {
+                            let val = st.widgets[idx].value();
+                            let mut changed = st.widgets[idx].keyboard_input(&custom_event);
+                            if st.widgets[idx].value() != val {
+                                changed = true;
+                            }
+                            if changed {
+                                st.upload_vertices();
+                                app.redraw = true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
         if app.redraw {
             app.redraw = false;
             if let Some(state) = &mut app.state {
diff --git a/src/widget.rs b/src/widget.rs
index b4d91d7..9e793bf 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -57,6 +57,7 @@ pub struct KeyEvent {
     pub text: Option<String>,
     pub repeat: bool,
     pub ctrl: bool,
+    pub shift: bool,
 }
 
 use crate::colors;
@@ -206,6 +207,255 @@ pub mod focus {
     }
 }
 
+pub mod clipboard {
+    pub fn copy_to_clipboard(text: &str) {
+        let text = text.to_string();
+        std::thread::spawn(move || {
+            if let Ok(mut child) = std::process::Command::new("wl-copy")
+                .stdin(std::process::Stdio::piped())
+                .spawn()
+            {
+                if let Some(mut stdin) = child.stdin.take() {
+                    use std::io::Write;
+                    let _ = stdin.write_all(text.as_bytes());
+                }
+                let _ = child.wait();
+            } else if let Ok(mut child) = std::process::Command::new("xclip")
+                .arg("-selection")
+                .arg("clipboard")
+                .stdin(std::process::Stdio::piped())
+                .spawn()
+            {
+                if let Some(mut stdin) = child.stdin.take() {
+                    use std::io::Write;
+                    let _ = stdin.write_all(text.as_bytes());
+                }
+                let _ = child.wait();
+            }
+        });
+    }
+
+    pub fn read_from_clipboard() -> Option<String> {
+        if let Ok(output) = std::process::Command::new("wl-paste")
+            .arg("-n")
+            .output()
+        {
+            if output.status.success() {
+                if let Ok(text) = String::from_utf8(output.stdout) {
+                    return Some(text);
+                }
+            }
+        }
+        if let Ok(output) = std::process::Command::new("xclip")
+            .arg("-selection")
+            .arg("clipboard")
+            .arg("-o")
+            .output()
+        {
+            if output.status.success() {
+                if let Ok(text) = String::from_utf8(output.stdout) {
+                    return Some(text);
+                }
+            }
+        }
+        None
+    }
+}
+
+pub mod context_menu {
+    use super::{Widget, TextBox, MouseButton, ElementState, TextLabel};
+    use std::cell::RefCell;
+
+    #[derive(Debug, Clone)]
+    pub struct ContextMenuState {
+        pub x: f32,
+        pub y: f32,
+        pub w: f32,
+        pub h: f32,
+        pub visible: bool,
+        pub options: Vec<String>,
+        pub hovered_item: Option<usize>,
+        pub target: Option<*mut TextBox>,
+    }
+
+    impl ContextMenuState {
+        pub fn new() -> Self {
+            Self {
+                x: 0.0,
+                y: 0.0,
+                w: 120.0,
+                h: 0.0,
+                visible: false,
+                options: Vec::new(),
+                hovered_item: None,
+                target: None,
+            }
+        }
+
+        pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+            self.x = x;
+            self.y = y;
+            self.options = options;
+            self.h = self.options.len() as f32 * 24.0;
+            self.visible = true;
+            self.hovered_item = None;
+            self.target = Some(target);
+        }
+
+        pub fn hide(&mut self) {
+            self.visible = false;
+            self.target = None;
+        }
+
+        pub fn hit_test(&self, px: f32, py: f32) -> bool {
+            if !self.visible { return false; }
+            px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
+        }
+
+        pub fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+            if !self.visible { return false; }
+            let was_hovered = self.hovered_item;
+            self.hovered_item = None;
+            if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
+                let idx = ((py - self.y) / 24.0) as usize;
+                if idx < self.options.len() {
+                    self.hovered_item = Some(idx);
+                }
+            }
+            self.hovered_item != was_hovered
+        }
+
+        pub fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+            if !self.visible { return false; }
+            if button != MouseButton::Left || state != ElementState::Pressed {
+                if state == ElementState::Pressed {
+                    self.hide();
+                    return true;
+                }
+                return false;
+            }
+
+            if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
+                let idx = ((py - self.y) / 24.0) as usize;
+                if idx < self.options.len() {
+                    let opt = self.options[idx].clone();
+                    if let Some(target_ptr) = self.target {
+                        unsafe {
+                            let target = &mut *target_ptr;
+                            match opt.as_str() {
+                                "Cut" => {
+                                    if target.cut_selection() {
+                                        target.just_changed = true;
+                                    }
+                                }
+                                "Copy" => {
+                                    target.copy_selection();
+                                }
+                                "Paste" => {
+                                    if target.paste_from_clipboard() {
+                                        target.just_changed = true;
+                                    }
+                                }
+                                "Select All" => {
+                                    target.select_all();
+                                }
+                                _ => {}
+                            }
+                        }
+                    }
+                }
+                self.hide();
+                return true;
+            } else {
+                self.hide();
+                return true;
+            }
+        }
+
+        pub fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+            let mut quads = Vec::new();
+            if !self.visible { return quads; }
+
+            // border
+            quads.push((self.x, self.y, self.w, self.h, [0.22, 0.22, 0.28, 1.0]));
+            // bg
+            quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, [0.06, 0.06, 0.09, 1.0]));
+
+            if let Some(h_idx) = self.hovered_item {
+                let iy = self.y + h_idx as f32 * 24.0;
+                quads.push((self.x + 2.0, iy + 2.0, self.w - 4.0, 20.0, [0.20, 0.40, 0.65, 0.6]));
+            }
+
+            quads
+        }
+
+        pub fn text_labels(&self) -> Vec<TextLabel> {
+            let mut labels = Vec::new();
+            if !self.visible { return labels; }
+
+            for (idx, opt) in self.options.iter().enumerate() {
+                let iy = self.y + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
+                let text_color = if self.hovered_item == Some(idx) {
+                    [0xff, 0xff, 0xff]
+                } else {
+                    [0xcc, 0xcc, 0xd4]
+                };
+
+                labels.push(TextLabel {
+                    text: opt.clone(),
+                    x: self.x + 8.0,
+                    y: iy,
+                    font_size: 12.0,
+                    color: text_color,
+                });
+            }
+            labels
+        }
+    }
+
+    thread_local! {
+        pub static CONTEXT_MENU: RefCell<ContextMenuState> = RefCell::new(ContextMenuState::new());
+    }
+
+    pub fn is_visible() -> bool {
+        CONTEXT_MENU.with(|m| m.borrow().visible)
+    }
+
+    pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+        CONTEXT_MENU.with(|m| m.borrow_mut().show(x, y, options, target));
+    }
+
+    pub fn hide() {
+        CONTEXT_MENU.with(|m| m.borrow_mut().hide());
+    }
+
+    pub fn x() -> f32 { CONTEXT_MENU.with(|m| m.borrow().x) }
+    pub fn y() -> f32 { CONTEXT_MENU.with(|m| m.borrow().y) }
+    pub fn w() -> f32 { CONTEXT_MENU.with(|m| m.borrow().w) }
+    pub fn h() -> f32 { CONTEXT_MENU.with(|m| m.borrow().h) }
+    pub fn hovered_item() -> Option<usize> { CONTEXT_MENU.with(|m| m.borrow().hovered_item) }
+    pub fn options() -> Vec<String> { CONTEXT_MENU.with(|m| m.borrow().options.clone()) }
+
+    pub fn hit_test(px: f32, py: f32) -> bool {
+        CONTEXT_MENU.with(|m| m.borrow().hit_test(px, py))
+    }
+
+    pub fn cursor_moved(px: f32, py: f32) -> bool {
+        CONTEXT_MENU.with(|m| m.borrow_mut().cursor_moved(px, py))
+    }
+
+    pub fn mouse_input(button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        CONTEXT_MENU.with(|m| m.borrow_mut().mouse_input(button, state, px, py))
+    }
+
+    pub fn extra_quads() -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        CONTEXT_MENU.with(|m| m.borrow().extra_quads())
+    }
+
+    pub fn text_labels() -> Vec<TextLabel> {
+        CONTEXT_MENU.with(|m| m.borrow().text_labels())
+    }
+}
 
 #[derive(Debug, Clone)]
 pub struct TextLabel {
@@ -247,6 +497,7 @@ pub trait Widget {
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
     fn text_labels(&self) -> Vec<TextLabel> { Vec::new() }
+    fn widget_font(&self) -> Option<String> { None }
     fn value(&self) -> i32 { 0 }
     fn top_room(&self) -> f32 { 0.0 }
 
@@ -2225,6 +2476,7 @@ impl Widget for Spinbox {
     fn value(&self) -> i32 { self.value }
     fn set_hovered(&mut self, v: bool) { self.hovered = v; }
     fn hovered(&self) -> bool { self.hovered }
+    fn widget_font(&self) -> Option<String> { Some("monospace".to_string()) }
 
     fn hit_test(&self, px: f32, py: f32) -> bool {
         let (x, y, w, h) = self.rect();
@@ -2341,14 +2593,12 @@ impl Widget for Spinbox {
             }
             _ => {
                 if let Some(text) = &event.text {
-                    if !event.repeat {
-                        for ch in text.chars() {
-                            match ch {
-                                '-' if self.edit_buffer.is_empty() => self.edit_buffer.push('-'),
-                                '.' if self.decimals > 0 && !self.edit_buffer.contains('.') => self.edit_buffer.push('.'),
-                                '0'..='9' => self.edit_buffer.push(ch),
-                                _ => {}
-                            }
+                    for ch in text.chars() {
+                        match ch {
+                            '-' if self.edit_buffer.is_empty() => self.edit_buffer.push('-'),
+                            '.' if self.decimals > 0 && !self.edit_buffer.contains('.') => self.edit_buffer.push('.'),
+                            '0'..='9' => self.edit_buffer.push(ch),
+                            _ => {}
                         }
                     }
                 }
@@ -2400,8 +2650,8 @@ impl Widget for Spinbox {
             quads.push((self.x + self.w * 0.55 - 1.0, self.y, 1.0, self.h, border_color));
 
             // Caret cursor
-            let char_width = 8.0;
-            let cursor_x = self.x + 6.0 + (self.edit_buffer.len() as f32 * char_width);
+            let char_width = 8.4;
+            let cursor_x = self.x + 4.0 + (self.edit_buffer.len() as f32 * char_width);
             let max_cursor_x = split - 4.0;
             let final_cursor_x = cursor_x.min(max_cursor_x);
             let cursor_y = self.y + (self.h - 14.0) / 2.0;
@@ -2626,15 +2876,13 @@ impl Widget for ColorSelector {
             }
             _ => {
                 if let Some(text) = &event.text {
-                    if !event.repeat {
-                        for ch in text.chars() {
-                            match ch {
-                                '#' if self.edit_buffer.is_empty() => self.edit_buffer.push('#'),
-                                '0'..='9' | 'a'..='f' | 'A'..='F' => {
-                                    if self.edit_buffer.len() < 7 { self.edit_buffer.push(ch.to_ascii_lowercase()); }
-                                }
-                                _ => {}
+                    for ch in text.chars() {
+                        match ch {
+                            '#' if self.edit_buffer.is_empty() => self.edit_buffer.push('#'),
+                            '0'..='9' | 'a'..='f' | 'A'..='F' => {
+                                if self.edit_buffer.len() < 7 { self.edit_buffer.push(ch.to_ascii_lowercase()); }
                             }
+                            _ => {}
                         }
                     }
                 }
@@ -3707,6 +3955,140 @@ mod tests {
         assert_eq!(dd.selected, 1);
         assert!(dd.take_change());
     }
+
+    #[test]
+    fn test_textbox_selection_highlight() {
+        let mut tb = TextBox::new("Initial Text".to_string());
+        tb.set_rect(10.0, 10.0, 200.0, 30.0);
+
+        // 1. Initial state
+        assert!(!tb.editing);
+        assert!(!tb.all_selected);
+
+        // 2. Click focuses and triggers highlighting
+        let clicked = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
+        assert!(clicked);
+        assert!(tb.editing);
+        assert!(tb.all_selected);
+        assert_eq!(tb.edit_buffer, "Initial Text");
+
+        // 3. Typing a key replaces all text
+        let key_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Character("A".to_string()),
+            text: Some("A".to_string()),
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        let handled = tb.keyboard_input(&key_ev);
+        assert!(handled);
+        assert!(!tb.all_selected);
+        assert_eq!(tb.edit_buffer, "A");
+
+        // 4. Pressing Enter commits change
+        let enter_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::Enter),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        let handled_enter = tb.keyboard_input(&enter_ev);
+        assert!(handled_enter);
+        assert!(!tb.editing);
+        assert_eq!(tb.text, "A");
+        assert!(tb.take_change());
+    }
+
+    #[test]
+    fn test_textbox_drag_and_modifier_selection() {
+        let mut tb = TextBox::new("Hello World".to_string());
+        tb.set_rect(10.0, 10.0, 200.0, 30.0);
+
+        // 1. Initial click focuses and selects all
+        let pressed = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
+        assert!(pressed);
+        let released = tb.mouse_input(MouseButton::Left, ElementState::Released, 50.0, 20.0);
+        assert!(released);
+        assert!(tb.editing);
+        assert!(tb.all_selected);
+        assert_eq!(tb.cursor_idx, 11);
+        assert_eq!(tb.select_anchor, Some(0));
+
+        // 2. Click inside placed caret at index 5 (x = 10 + 8 + 5 * 7.2 = 54)
+        let pressed_inside = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 54.0, 20.0);
+        assert!(pressed_inside);
+        assert_eq!(tb.cursor_idx, 5);
+        assert_eq!(tb.select_anchor, Some(5));
+        assert!(!tb.all_selected);
+
+        // 3. Drag to index 11 (x = 10 + 8 + 11 * 7.2 = 97.2)
+        tb.drag_begin(54.0, 20.0);
+        let updated = tb.drag_update(97.2, 20.0);
+        assert!(updated);
+        assert_eq!(tb.cursor_idx, 11);
+        assert_eq!(tb.select_anchor, Some(5));
+        tb.drag_end();
+
+        // 4. Keyboard ArrowLeft with Shift shrinks selection from 11 to 10
+        let left_shift_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::ArrowLeft),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: true,
+        };
+        let handled = tb.keyboard_input(&left_shift_ev);
+        assert!(handled);
+        assert_eq!(tb.cursor_idx, 10);
+        assert_eq!(tb.select_anchor, Some(5));
+
+        // 5. Keyboard ArrowLeft without Shift collapses selection to start (index 5)
+        let left_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::ArrowLeft),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        let handled = tb.keyboard_input(&left_ev);
+        assert!(handled);
+        assert_eq!(tb.cursor_idx, 5);
+        assert_eq!(tb.select_anchor, None);
+
+        // 6. Keyboard Shift+Up highlights to beginning (cursor 0, anchor 5)
+        let up_shift_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::ArrowUp),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: true,
+        };
+        let handled = tb.keyboard_input(&up_shift_ev);
+        assert!(handled);
+        assert_eq!(tb.cursor_idx, 0);
+        assert_eq!(tb.select_anchor, Some(5));
+
+        // 7. Typing a key replaces selected range "Hello" with "Rust"
+        let rust_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Character("Rust".to_string()),
+            text: Some("Rust".to_string()),
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        let handled = tb.keyboard_input(&rust_ev);
+        assert!(handled);
+        assert_eq!(tb.edit_buffer, "Rust World");
+        assert_eq!(tb.cursor_idx, 4);
+        assert_eq!(tb.select_anchor, None);
+    }
 }
 
 // Generic text item layout wrapper
@@ -3731,9 +4113,14 @@ pub struct StyledLabel {
 
 impl StyledLabel {
     pub fn new(fs: &mut glyphon::FontSystem, text: &str, size: f32, color: [f32; 4]) -> Self {
+        Self::new_with_family(fs, text, size, color, "sans-serif")
+    }
+
+    pub fn new_with_family(fs: &mut glyphon::FontSystem, text: &str, size: f32, color: [f32; 4], family: &str) -> Self {
         let metrics = glyphon::Metrics::new(size, size * 1.4);
         let mut buffer = glyphon::Buffer::new(fs, metrics);
-        buffer.set_text(fs, text, glyphon::Attrs::new(), glyphon::Shaping::Advanced);
+        let attrs = glyphon::Attrs::new().family(glyphon::Family::Name(family));
+        buffer.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
         buffer.shape_until_scroll(fs, true);
         let w = buffer.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
         let g_color = glyphon::Color::rgb(
@@ -4224,13 +4611,752 @@ impl Widget for Dropdown {
     fn clear_children(&mut self) { self.children.clear(); }
 }
 
-impl Drop for Dropdown {
+unsafe impl Send for Dropdown {}
+unsafe impl Sync for Dropdown {}
+
+// ── TextBox Widget ──
+
+#[derive(Debug, Clone)]
+pub struct TextBox {
+    x: f32, y: f32, w: f32, h: f32,
+    pub text: String,
+    pub editing: bool,
+    pub edit_buffer: String,
+    hovered: bool,
+    just_changed: bool,
+    label: Option<String>,
+    row_x: f32,
+    row_w: f32,
+    pub disabled: bool,
+    pub all_selected: bool,
+    pub cursor_idx: usize,
+    pub select_anchor: Option<usize>,
+    pub dragging: bool,
+    pub just_focused: bool,
+    pub drag_start_idx: Option<usize>,
+    pub parent: Option<*mut (dyn Widget + 'static)>,
+    pub children: Vec<*mut (dyn Widget + 'static)>,
+    pub max_width: Option<f32>,
+    pub width: Option<f32>,
+}
+
+impl TextBox {
+    pub fn new(text: String) -> Self {
+        Self {
+            x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+            text,
+            editing: false,
+            edit_buffer: String::new(),
+            hovered: false,
+            just_changed: false,
+            label: None,
+            row_x: 0.0,
+            row_w: 0.0,
+            disabled: false,
+            all_selected: false,
+            cursor_idx: 0,
+            select_anchor: None,
+            dragging: false,
+            just_focused: false,
+            drag_start_idx: None,
+            parent: None,
+            children: Vec::new(),
+            max_width: Some(300.0),
+            width: None,
+        }
+    }
+
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.label = Some(label.to_string());
+        self
+    }
+
+    pub fn set_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
+    pub fn take_change(&mut self) -> bool {
+        let changed = self.just_changed;
+        self.just_changed = false;
+        changed
+    }
+
+    pub fn with_max_width(mut self, max_w: Option<f32>) -> Self {
+        self.max_width = max_w;
+        self
+    }
+
+    pub fn set_max_width(&mut self, max_w: Option<f32>) {
+        self.max_width = max_w;
+    }
+
+    pub fn with_width(mut self, w: f32) -> Self {
+        self.width = Some(w);
+        self
+    }
+
+    pub fn set_width(&mut self, w: f32) {
+        self.width = Some(w);
+    }
+
+    pub fn copy_selection(&self) {
+        let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+        let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+        if start != end {
+            let chars: Vec<char> = self.edit_buffer.chars().collect();
+            let selected_text: String = chars[start..end].iter().collect();
+            clipboard::copy_to_clipboard(&selected_text);
+        }
+    }
+
+    pub fn cut_selection(&mut self) -> bool {
+        let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+        let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+        if start != end {
+            let chars: Vec<char> = self.edit_buffer.chars().collect();
+            let selected_text: String = chars[start..end].iter().collect();
+            clipboard::copy_to_clipboard(&selected_text);
+
+            let mut new_buf = String::new();
+            for i in 0..start {
+                new_buf.push(chars[i]);
+            }
+            for i in end..chars.len() {
+                new_buf.push(chars[i]);
+            }
+            self.edit_buffer = new_buf;
+            self.cursor_idx = start;
+            self.select_anchor = None;
+            self.all_selected = false;
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn paste_from_clipboard(&mut self) -> bool {
+        if let Some(text) = clipboard::read_from_clipboard() {
+            let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+            let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+            let chars: Vec<char> = self.edit_buffer.chars().collect();
+            let mut new_buf = String::new();
+            for i in 0..start {
+                new_buf.push(chars[i]);
+            }
+            let mut inserted_count = 0;
+            for ch in text.chars() {
+                if !ch.is_control() && ch != '\n' && ch != '\r' {
+                    new_buf.push(ch);
+                    inserted_count += 1;
+                }
+            }
+            for i in end..chars.len() {
+                new_buf.push(chars[i]);
+            }
+            self.edit_buffer = new_buf;
+            self.cursor_idx = start + inserted_count;
+            self.select_anchor = None;
+            self.all_selected = false;
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn select_all(&mut self) {
+        let len = self.edit_buffer.chars().count();
+        self.select_anchor = Some(0);
+        self.cursor_idx = len;
+        self.all_selected = len > 0;
+    }
+}
+
+impl Default for TextBox {
+    fn default() -> Self {
+        Self::new(String::new())
+    }
+}
+
+impl Widget for TextBox {
+    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.x = x;
+        self.y = y;
+        self.w = if let Some(explicit_w) = self.width {
+            explicit_w
+        } else if let Some(max_w) = self.max_width {
+            w.min(max_w)
+        } else {
+            w
+        };
+        self.h = h;
+    }
+    fn set_row_rect(&mut self, x: f32, w: f32) {
+        self.row_x = x;
+        self.row_w = if let Some(explicit_w) = self.width {
+            explicit_w
+        } else if let Some(max_w) = self.max_width {
+            w.min(max_w)
+        } else {
+            w
+        };
+    }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+
+    fn color(&self) -> [f32; 4] {
+        [0.10, 0.10, 0.16, 1.0]
+    }
+
+    fn hit_test(&self, px: f32, py: f32) -> bool {
+        let (x, y, w, h) = self.rect();
+        let hx = if self.row_w > 0.0 { self.row_x } else { x };
+        let hw = if self.row_w > 0.0 { self.row_w } else { w };
+        let (hy, hh) = if self.label.is_some() {
+            (y - 18.0, h + 18.0)
+        } else {
+            (y, h)
+        };
+        px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
+    }
+
+    fn top_room(&self) -> f32 { if self.label.is_some() { 18.0 } else { 0.0 } }
+
+    fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+        if self.disabled {
+            let was = self.hovered;
+            self.hovered = false;
+            return was;
+        }
+        let mut changed = false;
+        if self.dragging && self.editing {
+            let char_width = 7.2;
+            let drag_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+                .max(0)
+                .min(self.edit_buffer.chars().count() as isize) as usize;
+            if self.cursor_idx != drag_idx {
+                self.cursor_idx = drag_idx;
+                self.just_focused = false;
+                let len = self.edit_buffer.chars().count();
+                let start = self.select_anchor.unwrap_or(0).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(0).max(self.cursor_idx);
+                self.all_selected = start == 0 && end == len && len > 0;
+                changed = true;
+            }
+        }
+        let was = self.hovered;
+        self.hovered = self.hit_test(px, py);
+        if was != self.hovered {
+            changed = true;
+        }
+        changed
+    }
+
+    fn draggable(&self) -> bool { !self.disabled }
+    fn is_dragging(&self) -> bool { self.dragging }
+    fn widget_font(&self) -> Option<String> { Some("monospace".to_string()) }
+
+    fn drag_begin(&mut self, _px: f32, _py: f32) {
+        if self.disabled || !self.editing { return; }
+        self.dragging = true;
+    }
+
+    fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+        if self.disabled || !self.editing { return false; }
+        let char_width = 7.2;
+        let drag_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+            .max(0)
+            .min(self.edit_buffer.chars().count() as isize) as usize;
+        if self.cursor_idx != drag_idx {
+            self.cursor_idx = drag_idx;
+            self.just_focused = false;
+            let len = self.edit_buffer.chars().count();
+            let start = self.select_anchor.unwrap_or(0).min(self.cursor_idx);
+            let end = self.select_anchor.unwrap_or(0).max(self.cursor_idx);
+            self.all_selected = start == 0 && end == len && len > 0;
+            return true;
+        }
+        false
+    }
+
+    fn drag_end(&mut self) {
+        self.dragging = false;
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+        if self.disabled { return false; }
+        
+        let (x, y, w, h) = self.rect();
+        let (hy, hh) = if self.label.is_some() {
+            (y - 18.0, h + 18.0)
+        } else {
+            (y, h)
+        };
+        let is_inside = px >= x && px <= x + w && py >= hy && py <= hy + hh;
+
+        if button == MouseButton::Right {
+            if state == ElementState::Pressed {
+                if !is_inside {
+                    return false;
+                }
+                
+                // Focus the TextBox if it wasn't editing yet
+                if !self.editing {
+                    self.editing = true;
+                    self.edit_buffer = self.text.clone();
+                    let len = self.edit_buffer.chars().count();
+                    self.cursor_idx = len;
+                    self.select_anchor = Some(0);
+                    self.all_selected = len > 0;
+                    self.just_focused = true;
+                    focus::set_focused(self);
+                }
+
+                // Place cursor under click if clicked inside the content bounds
+                if px >= x && px <= x + w && py >= y && py <= y + h {
+                    let char_width = 7.2;
+                    let click_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+                        .max(0)
+                        .min(self.edit_buffer.chars().count() as isize) as usize;
+                    self.cursor_idx = click_idx;
+                    self.select_anchor = Some(click_idx);
+                    self.all_selected = false;
+                }
+                
+                // Spawn context menu
+                let options = vec![
+                    "Cut".to_string(),
+                    "Copy".to_string(),
+                    "Paste".to_string(),
+                    "Select All".to_string(),
+                ];
+                context_menu::show(px, py, options, self as *mut TextBox);
+                return true;
+            }
+            return false;
+        }
+
+        if button != MouseButton::Left { return false; }
+        
+        let (x, y, w, h) = self.rect();
+        let (hy, hh) = if self.label.is_some() {
+            (y - 18.0, h + 18.0)
+        } else {
+            (y, h)
+        };
+        let is_inside = px >= x && px <= x + w && py >= hy && py <= hy + hh;
+
+        if state == ElementState::Pressed {
+            if !is_inside {
+                if self.editing {
+                    self.unfocus();
+                    return true;
+                }
+                return false;
+            }
+            
+            let char_width = 7.2;
+            let click_idx = (((px - (self.x + 8.0)) / char_width).round() as isize)
+                .max(0)
+                .min(self.edit_buffer.chars().count() as isize) as usize;
+            
+            if self.editing {
+                self.cursor_idx = click_idx;
+                self.select_anchor = Some(click_idx);
+                self.all_selected = false;
+                self.just_focused = false;
+                self.drag_start_idx = Some(click_idx);
+            } else {
+                self.editing = true;
+                self.edit_buffer = self.text.clone();
+                let len = self.edit_buffer.chars().count();
+                self.cursor_idx = len;
+                self.select_anchor = Some(0);
+                self.all_selected = len > 0;
+                self.just_focused = true;
+                self.drag_start_idx = Some(click_idx);
+                focus::set_focused(self);
+            }
+            true
+        } else if state == ElementState::Released {
+            if is_inside && self.editing && self.just_focused {
+                let len = self.edit_buffer.chars().count();
+                self.cursor_idx = len;
+                self.select_anchor = Some(0);
+                self.all_selected = len > 0;
+            }
+            self.just_focused = false;
+            self.drag_start_idx = None;
+            true
+        } else {
+            false
+        }
+    }
+
+    fn focus(&mut self) {
+        if self.disabled { return; }
+        if !self.editing {
+            self.editing = true;
+            self.edit_buffer = self.text.clone();
+            let len = self.edit_buffer.chars().count();
+            self.cursor_idx = len;
+            self.select_anchor = Some(0);
+            self.all_selected = len > 0;
+            self.just_focused = true;
+            focus::set_focused(self);
+        }
+    }
+
+    fn unfocus(&mut self) {
+        if self.editing {
+            self.editing = false;
+            self.all_selected = false;
+            self.select_anchor = None;
+            self.dragging = false;
+            self.just_focused = false;
+            self.drag_start_idx = None;
+            if self.text != self.edit_buffer {
+                self.text = self.edit_buffer.clone();
+                self.just_changed = true;
+            }
+        }
+    }
+
+    fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+        if self.disabled { return false; }
+        if !self.editing { return false; }
+        if event.state != ElementState::Pressed { return false; }
+        let char_count = self.edit_buffer.chars().count();
+        match &event.logical_key {
+            Key::Named(NamedKey::Backspace) => {
+                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                if start != end {
+                    let chars: Vec<char> = self.edit_buffer.chars().collect();
+                    let mut new_buf = String::new();
+                    for i in 0..start {
+                        new_buf.push(chars[i]);
+                    }
+                    for i in end..chars.len() {
+                        new_buf.push(chars[i]);
+                    }
+                    self.edit_buffer = new_buf;
+                    self.cursor_idx = start;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                } else if self.cursor_idx > 0 {
+                    let chars: Vec<char> = self.edit_buffer.chars().collect();
+                    let mut new_buf = String::new();
+                    for i in 0..(self.cursor_idx - 1) {
+                        new_buf.push(chars[i]);
+                    }
+                    for i in self.cursor_idx..chars.len() {
+                        new_buf.push(chars[i]);
+                    }
+                    self.edit_buffer = new_buf;
+                    self.cursor_idx -= 1;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                }
+                true
+            }
+            Key::Named(NamedKey::Delete) => {
+                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                if start != end {
+                    let chars: Vec<char> = self.edit_buffer.chars().collect();
+                    let mut new_buf = String::new();
+                    for i in 0..start {
+                        new_buf.push(chars[i]);
+                    }
+                    for i in end..chars.len() {
+                        new_buf.push(chars[i]);
+                    }
+                    self.edit_buffer = new_buf;
+                    self.cursor_idx = start;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                } else if self.cursor_idx < char_count {
+                    let chars: Vec<char> = self.edit_buffer.chars().collect();
+                    let mut new_buf = String::new();
+                    for i in 0..self.cursor_idx {
+                        new_buf.push(chars[i]);
+                    }
+                    for i in (self.cursor_idx + 1)..chars.len() {
+                        new_buf.push(chars[i]);
+                    }
+                    self.edit_buffer = new_buf;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                }
+                true
+            }
+            Key::Named(NamedKey::ArrowLeft) => {
+                if event.shift {
+                    if self.select_anchor.is_none() {
+                        self.select_anchor = Some(self.cursor_idx);
+                    }
+                    if self.cursor_idx > 0 {
+                        self.cursor_idx -= 1;
+                    }
+                } else {
+                    let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                    let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                    if start != end {
+                        self.cursor_idx = start;
+                    } else if self.cursor_idx > 0 {
+                        self.cursor_idx -= 1;
+                    }
+                    self.select_anchor = None;
+                }
+                let len = self.edit_buffer.chars().count();
+                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                self.all_selected = start == 0 && end == len && len > 0;
+                true
+            }
+            Key::Named(NamedKey::ArrowRight) => {
+                if event.shift {
+                    if self.select_anchor.is_none() {
+                        self.select_anchor = Some(self.cursor_idx);
+                    }
+                    if self.cursor_idx < char_count {
+                        self.cursor_idx += 1;
+                    }
+                } else {
+                    let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                    let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                    if start != end {
+                        self.cursor_idx = end;
+                    } else if self.cursor_idx < char_count {
+                        self.cursor_idx += 1;
+                    }
+                    self.select_anchor = None;
+                }
+                let len = self.edit_buffer.chars().count();
+                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                self.all_selected = start == 0 && end == len && len > 0;
+                true
+            }
+            Key::Named(NamedKey::ArrowUp) | Key::Named(NamedKey::Home) => {
+                if event.shift {
+                    if self.select_anchor.is_none() {
+                        self.select_anchor = Some(self.cursor_idx);
+                    }
+                    self.cursor_idx = 0;
+                } else {
+                    self.cursor_idx = 0;
+                    self.select_anchor = None;
+                }
+                let len = self.edit_buffer.chars().count();
+                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                self.all_selected = start == 0 && end == len && len > 0;
+                true
+            }
+            Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::End) => {
+                if event.shift {
+                    if self.select_anchor.is_none() {
+                        self.select_anchor = Some(self.cursor_idx);
+                    }
+                    self.cursor_idx = char_count;
+                } else {
+                    self.cursor_idx = char_count;
+                    self.select_anchor = None;
+                }
+                let len = self.edit_buffer.chars().count();
+                let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                self.all_selected = start == 0 && end == len && len > 0;
+                true
+            }
+            Key::Named(NamedKey::Enter) => {
+                self.text = self.edit_buffer.clone();
+                self.editing = false;
+                self.all_selected = false;
+                self.select_anchor = None;
+                self.just_changed = true;
+                true
+            }
+            Key::Named(NamedKey::Escape) => {
+                self.editing = false;
+                self.all_selected = false;
+                self.select_anchor = None;
+                true
+            }
+            Key::Character(ref s) if event.ctrl => {
+                match s.as_str() {
+                    "c" | "C" => {
+                        self.copy_selection();
+                        true
+                    }
+                    "x" | "X" => {
+                        if self.cut_selection() {
+                            self.just_changed = true;
+                        }
+                        true
+                    }
+                    "v" | "V" => {
+                        if self.paste_from_clipboard() {
+                            self.just_changed = true;
+                        }
+                        true
+                    }
+                    "a" | "A" => {
+                        self.select_all();
+                        true
+                    }
+                    _ => false,
+                }
+            }
+            _ => {
+                if event.ctrl {
+                    return false;
+                }
+                if let Some(text) = &event.text {
+                    let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+                    let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+                    let chars: Vec<char> = self.edit_buffer.chars().collect();
+                    let mut new_buf = String::new();
+                    for i in 0..start {
+                        new_buf.push(chars[i]);
+                    }
+                    let mut inserted_count = 0;
+                    for ch in text.chars() {
+                        if ch.is_alphanumeric() || ch == ' ' || ch == '-' || ch == '_' || ch == '*' {
+                            new_buf.push(ch);
+                            inserted_count += 1;
+                        }
+                    }
+                    for i in end..chars.len() {
+                        new_buf.push(chars[i]);
+                    }
+                    self.edit_buffer = new_buf;
+                    self.cursor_idx = start + inserted_count;
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                } else {
+                    self.select_anchor = None;
+                    self.all_selected = false;
+                }
+                true
+            }
+        }
+    }
+
+    fn hover_highlight(&self) -> Option<[f32; 4]> {
+        None
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        if self.disabled {
+            quads.push((self.x, self.y, self.w, self.h, [0.12, 0.12, 0.16, 1.0]));
+            quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, [0.06, 0.06, 0.08, 1.0]));
+            return quads;
+        }
+        if self.hovered {
+            let (hy, hh) = if self.label.is_some() {
+                (self.y - 18.0, self.h + 18.0)
+            } else {
+                (self.y, self.h)
+            };
+            let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
+            let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
+            quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]));
+        }
+        let bg_color = if self.editing {
+            [0.06, 0.10, 0.18, 1.0]
+        } else {
+            [0.08, 0.08, 0.12, 1.0]
+        };
+        let border_color = if self.editing {
+            [0.20, 0.50, 0.85, 1.0]
+        } else if self.hovered {
+            [0.25, 0.25, 0.35, 1.0]
+        } else {
+            [0.18, 0.18, 0.24, 1.0]
+        };
+        quads.push((self.x, self.y, self.w, self.h, border_color));
+        quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, bg_color));
+
+        if self.editing {
+            let char_width = 7.2;
+            let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
+            let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
+            
+            if start != end {
+                let highlight_x = self.x + 8.0 + (start as f32 * char_width);
+                let max_x = self.x + self.w - 6.0;
+                let highlight_w = ((end - start) as f32 * char_width).min(max_x - highlight_x).max(0.0);
+                quads.push((
+                    highlight_x,
+                    self.y + (self.h - 16.0) / 2.0,
+                    highlight_w,
+                    16.0,
+                    [0.20, 0.40, 0.75, 0.4],
+                ));
+            }
+
+            let cursor_x = self.x + 8.0 + (self.cursor_idx as f32 * char_width);
+            let max_cursor_x = self.x + self.w - 6.0;
+            let final_cursor_x = cursor_x.min(max_cursor_x);
+            let cursor_y = self.y + (self.h - 14.0) / 2.0;
+            quads.push((final_cursor_x, cursor_y, 1.5, 14.0, [0.80, 0.80, 0.85, 1.0]));
+        }
+
+        quads
+    }
+
+    fn text_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        if let Some(ref label) = self.label {
+            labels.push(TextLabel {
+                text: label.clone(),
+                x: self.x + 4.0,
+                y: self.y - 14.0,
+                font_size: 11.0,
+                color: [0x83, 0x83, 0x8a],
+            });
+        }
+        let val_text = if self.editing {
+            self.edit_buffer.clone()
+        } else {
+            self.text.clone()
+        };
+        labels.push(TextLabel {
+            text: val_text,
+            x: self.x + 8.0,
+            y: self.y + (self.h - 12.0) / 2.0,
+            font_size: 12.0,
+            color: if self.disabled {
+                [0x53, 0x53, 0x5a]
+            } else if self.all_selected {
+                [0xff, 0xff, 0xff]
+            } else if self.editing {
+                [0xee, 0xee, 0xf5]
+            } else {
+                [0xcc, 0xcc, 0xd4]
+            },
+        });
+        labels
+    }
+
+    fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
+    fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
+    fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
+    fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
+    fn clear_children(&mut self) { self.children.clear(); }
+}
+
+impl Drop for TextBox {
     fn drop(&mut self) {
         focus::clear_if_matches(self);
     }
 }
 
-unsafe impl Send for Dropdown {}
-unsafe impl Sync for Dropdown {}
+unsafe impl Send for TextBox {}
+unsafe impl Sync for TextBox {}