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

commit22bb8c6eec85e5993594831a66a7c5a19c659bbc
parent071c324e90
authorLucas Galante <[email protected]>
date2026-07-09 00:27
feat(widget): migrate TextBox (Phase 5q)

Widest-surface leaf so far. New adapter surface: Input clipboard quintet
(cut/copy/paste/select-all/clear — defaults replicate the whole-value
Element defaults), Paint::prepare_text (glyph shaping is load-bearing for
cursor<->pixel mapping), Layout::hit_row_rect (restores the legacy row-hit
geometry the earlier migrations dropped — cce-files' save-name box needs
it), Layout::adjust_row_rect + rect_assigned (width clamp on both rect
paths; ungated scroll re-clamp), Input::tracks_base_focus (legacy TextBox
never set base.focused), and Paint::legacy_focus_highlight — the legacy
shared focus-highlight overlay, suppressed for all migrated widgets, is
re-enabled per-widget: legacy TextBox kept the Element default and the
focused editor's primary-tint wash (data-editor's teal editing surface)
is real behavior, found when the first A/B came back 1.4M pixels apart.

Render split preserved faithfully (non-rounded: full-width bg + disabled
branch; rounded: side-label inset, no disabled branch). Flagged
approximations: release containment re-checked against the plain rect
(legacy hit-gated releases through the row-substituted test), wheel now
hit-gated by the adapter.

In-crate sweep: treelist, scrolling_list, keybinds_control,
multi_control (InstancedWidget variant), parameters_bg.

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

 src/widget/container/parameters_bg.rs  |    2 +-
 src/widget/container/scrolling_list.rs |    6 +-
 src/widget/container/treelist.rs       |   18 +-
 src/widget/input/keybinds_control.rs   |    4 +-
 src/widget/input/multi_control.rs      |    4 +-
 src/widget/input/text_box.rs           | 1756 ++++++++++++++++----------------
 src/widget/model.rs                    |  168 ++-
 7 files changed, 1037 insertions(+), 921 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 4f09912..414a913 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -15,7 +15,7 @@ pub struct ParametersBg {
     pub spinboxes: Vec<Option<crate::widget::Adapted<Spinbox>>>,
     pub buttons: Vec<Option<crate::widget::Adapted<Button>>>,
     pub choices: Vec<Option<crate::widget::Adapted<Dropdown>>>,
-    pub texts: Vec<Option<TextBox>>,
+    pub texts: Vec<Option<crate::widget::Adapted<TextBox>>>,
     pub checkboxes: Vec<Option<crate::widget::Adapted<Checkbox>>>,
     pub colors: Vec<Option<ColorSelector>>,
     visible: bool,
diff --git a/src/widget/container/scrolling_list.rs b/src/widget/container/scrolling_list.rs
index 64b586e..d5e0a6c 100644
--- a/src/widget/container/scrolling_list.rs
+++ b/src/widget/container/scrolling_list.rs
@@ -40,7 +40,7 @@ pub struct List {
     pub last_click_time: Option<std::time::Instant>,
     pub search_enabled: bool,
     pub search_visible: bool,
-    pub search_box: TextBox,
+    pub search_box: crate::widget::Adapted<TextBox>,
 }
 
 impl List {
@@ -690,7 +690,7 @@ impl Element for List {
             let self_ptr = self as *mut Self;
             let self_id = self.base.id();
             unsafe {
-                let sb_ptr = &mut (*self_ptr).search_box as *mut TextBox as *mut (dyn Element + 'static);
+                let sb_ptr = (*self_ptr).search_box.as_ptr_mut();
                 let sb_id = (*self_ptr).search_box.base().unwrap().id();
                 ctx.register_widget(sb_id, sb_ptr);
                 ctx.link_ids(self_id, sb_id);
@@ -703,7 +703,7 @@ impl Element for List {
         if self.search_enabled && self.search_visible {
             let self_ptr = self as *const Self as *mut Self;
             unsafe {
-                list.push(&mut (*self_ptr).search_box as *mut TextBox as *mut (dyn Element + 'static));
+                list.push((*self_ptr).search_box.as_ptr_mut());
             }
         }
         list
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index ce18734..7c5574c 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -160,10 +160,10 @@ fn build_tree(
 pub struct TreeList {
     pub base: Widget,
     pub scroll_box: ScrollBox,
-    pub search_box: TextBox,
+    pub search_box: crate::widget::Adapted<TextBox>,
     pub add_key_btn: crate::widget::Adapted<Button>,
     pub add_key_popover_open: bool,
-    pub add_key_popover_box: TextBox,
+    pub add_key_popover_box: crate::widget::Adapted<TextBox>,
     pub new_key_path_request: Option<String>,
     pub flat_keys: Vec<(String, serde_json::Value)>,
     pub annotations: Vec<Option<String>>,
@@ -179,7 +179,7 @@ pub struct TreeList {
     pub last_scroll_y: f32,
     pub scrollbar_activity_timer: f32,
     pub deleted_key_path: Option<String>,
-    pub edit_box: TextBox,
+    pub edit_box: crate::widget::Adapted<TextBox>,
     pub editing_key_idx: Option<usize>,
     pub double_click_timer: Option<(std::time::Instant, usize)>,
     pub rename_request: Option<(String, String)>,
@@ -540,7 +540,7 @@ impl Element for TreeList {
                         let self_ptr = self as *mut Self;
                         let self_id = self.base.id();
                         unsafe {
-                            let eb_ptr = &mut (*self_ptr).edit_box as *mut TextBox as *mut (dyn Element + 'static);
+                            let eb_ptr = (*self_ptr).edit_box.as_ptr_mut();
                             let eb_id = (*self_ptr).edit_box.base().unwrap().id();
                             ctx.register_widget(eb_id, eb_ptr);
                             ctx.link_ids(self_id, eb_id);
@@ -1159,7 +1159,7 @@ impl Element for TreeList {
             let self_ptr = self as *mut Self;
             let self_id = self.base.id();
             unsafe {
-                let sb_ptr = &mut (*self_ptr).search_box as *mut TextBox as *mut (dyn Element + 'static);
+                let sb_ptr = (*self_ptr).search_box.as_ptr_mut();
                 let sb_id = (*self_ptr).search_box.base().unwrap().id();
                 ctx.register_widget(sb_id, sb_ptr);
                 ctx.link_ids(self_id, sb_id);
@@ -1171,7 +1171,7 @@ impl Element for TreeList {
                 ctx.link_ids(self_id, btn_id);
                 (*btn_ptr).set_parent(Some(self_ptr), ctx);
 
-                let pop_ptr = &mut (*self_ptr).add_key_popover_box as *mut TextBox as *mut (dyn Element + 'static);
+                let pop_ptr = (*self_ptr).add_key_popover_box.as_ptr_mut();
                 let pop_id = (*self_ptr).add_key_popover_box.base().unwrap().id();
                 ctx.register_widget(pop_id, pop_ptr);
                 ctx.link_ids(self_id, pop_id);
@@ -1184,13 +1184,13 @@ impl Element for TreeList {
         let mut list = self.children.clone();
         let self_ptr = self as *const Self as *mut Self;
         unsafe {
-            list.push(&mut (*self_ptr).search_box as *mut TextBox as *mut (dyn Element + 'static));
+            list.push((*self_ptr).search_box.as_ptr_mut());
             list.push((*self_ptr).add_key_btn.as_ptr_mut());
             if (*self_ptr).add_key_popover_open {
-                list.push(&mut (*self_ptr).add_key_popover_box as *mut TextBox as *mut (dyn Element + 'static));
+                list.push((*self_ptr).add_key_popover_box.as_ptr_mut());
             }
             if (*self_ptr).editing_key_idx.is_some() {
-                list.push(&mut (*self_ptr).edit_box as *mut TextBox as *mut (dyn Element + 'static));
+                list.push((*self_ptr).edit_box.as_ptr_mut());
             }
         }
         list
diff --git a/src/widget/input/keybinds_control.rs b/src/widget/input/keybinds_control.rs
index 3693c25..79bae98 100644
--- a/src/widget/input/keybinds_control.rs
+++ b/src/widget/input/keybinds_control.rs
@@ -6,8 +6,8 @@ use crate::widget::TextLabel;
 
 #[derive(Clone, Debug)]
 pub struct KeybindRow {
-    pub key_input: TextBox,
-    pub cmd_input: TextBox,
+    pub key_input: crate::widget::Adapted<TextBox>,
+    pub cmd_input: crate::widget::Adapted<TextBox>,
     pub remove_button: Adapted<Button>,
 }
 
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index deb86cb..66c339b 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -12,7 +12,7 @@ pub struct InstancedControl {
 
 #[derive(Clone, Debug)]
 pub enum InstancedWidget {
-    TextBox(TextBox),
+    TextBox(Adapted<TextBox>),
     Spinbox(Adapted<Spinbox>),
     Toggle(Adapted<Toggle>),
     Slider(Adapted<Slider>),
@@ -130,7 +130,7 @@ impl InstancedWidget {
 
 #[derive(Clone, Debug)]
 pub struct MultiControlRow {
-    pub key_input: TextBox,
+    pub key_input: Adapted<TextBox>,
     pub type_dropdown: Adapted<Dropdown>,
     pub value_widget: InstancedWidget,
     pub remove_button: Adapted<Button>,
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 7dfb674..6f32092 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1,4 +1,27 @@
+//! Narrow-trait `TextBox` (Phase 5q). The widest-surface leaf so far: real selection-aware
+//! clipboard (the new `Input` cut/copy/paste/select-all/clear hooks — their defaults replicate
+//! the whole-value `Element` defaults for everyone else), load-bearing glyph shaping through
+//! `Paint::prepare_text` (cursor↔pixel mapping reads the measured advances), the row-hit
+//! restoration (`Layout::hit_row_rect` — cce-files' save-name box relies on row hits), a
+//! width/max-width clamp on both rect paths (`Layout::adjust_rect` + `adjust_row_rect`), the
+//! ungated `Layout::rect_assigned` (scroll re-clamp on every `set_rect`, hidden or not), and
+//! `Input::tracks_base_focus = false` (legacy `focus()` never set the base flag — the detached
+//! label must not color as focused).
+//!
+//! Parity notes:
+//! - The legacy render split is asymmetric and preserved faithfully: the non-rounded path
+//!   (`extra_quads`) draws at the full base x/width with a disabled special-case; the rounded
+//!   path (`all_rounded_quads`) insets by the side label and has NO disabled branch.
+//! - Releases: legacy `mouse_input` hit-gated releases too (out-of-rect releases were dropped).
+//!   The adapter delivers releases ungated, so the model re-checks containment itself against
+//!   the plain rect (the row-substituted release geometry is approximated — flagged).
+//! - Wheel scrolling is now hit-gated by the adapter (legacy hosts called `mouse_wheel`
+//!   directly on the hovered widget, so the gate should be a no-op in practice — flagged).
+
 use crate::widget::*;
+use crate::scene::layout::{Rect, Size};
+use crate::scene::paint::PaintCtx;
+use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
 use std::sync::OnceLock;
 
 static FONT_DB: OnceLock<resvg::usvg::fontdb::Database> = OnceLock::new();
@@ -12,9 +35,18 @@ pub fn get_font_db() -> &'static resvg::usvg::fontdb::Database {
     })
 }
 
+/// Side-layout label inset — the legacy `Element::label_x_offset` default for non-exempt
+/// widgets (TextBox was never in the exempt list).
+fn side_offset(label: &Option<String>) -> f32 {
+    if crate::layout::control_label_layout() == "side" && label.is_some() {
+        90.0
+    } else {
+        0.0
+    }
+}
+
 #[derive(Debug, Clone)]
 pub struct TextBox {
-    base: Widget,
     pub text: String,
     pub editing: bool,
     pub edit_buffer: String,
@@ -27,7 +59,6 @@ pub struct TextBox {
     pub just_focused: bool,
     pub drag_start_idx: Option<usize>,
     pub parent: Option<*mut (dyn Element + 'static)>,
-    pub children: Vec<*mut (dyn Element + 'static)>,
     pub max_width: Option<f32>,
     pub width: Option<f32>,
     pub is_password: bool,
@@ -46,14 +77,20 @@ pub struct TextBox {
     pub glyph_positions: Vec<f32>,
     pub total_text_width: f32,
     pub update_on_type: bool,
+    /// Synced control label ([`Paint::sync_label`]) — drives the side/detached offsets.
+    label: Option<String>,
+    /// Own hover flag, maintained from `MouseEnter`/`MouseLeave` (adapter bookkeeping).
+    hovered: bool,
+    /// The laid-out base rect, cached from [`Layout::rect_assigned`] — the cursor/scroll math
+    /// reads geometry between events, which the narrow traits don't otherwise carry.
+    rect: Rect,
 }
 
 impl TextBox {
-    pub fn new(text: String) -> Self {
+    pub fn new(text: String) -> Adapted<TextBox> {
         let (style_family, style_size) = crate::layout::control_label_font_detached_parsed();
         let editor_state = TextEditorState::new(text.clone());
-        Self {
-            base: Widget::new(),
+        Adapted::new(TextBox {
             text,
             editing: false,
             edit_buffer: String::new(),
@@ -66,7 +103,6 @@ impl TextBox {
             just_focused: false,
             drag_start_idx: None,
             parent: None,
-            children: Vec::new(),
             max_width: None,
             width: None,
             is_password: false,
@@ -85,29 +121,36 @@ impl TextBox {
             glyph_positions: Vec::new(),
             total_text_width: 0.0,
             update_on_type: false,
-        }
-    }
-
-    pub fn with_update_on_type(mut self, update: bool) -> Self {
-        self.update_on_type = update;
-        self
+            label: None,
+            hovered: false,
+            rect: Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 },
+        })
     }
 
-    pub fn with_multiline(mut self, multiline: bool) -> Self {
-        self.multiline = multiline;
-        self
+    /// The detached-label strip height — a replica of `Widget::label_offset` over the synced
+    /// label (zero in side layout or unlabeled).
+    fn label_top(&self) -> f32 {
+        if crate::layout::control_label_layout() == "side" {
+            return 0.0;
+        }
+        if self.label.is_some() {
+            let (_, font_size) = crate::layout::control_label_font_detached_parsed();
+            font_size + crate::layout::control_label_margin()
+        } else {
+            0.0
+        }
     }
 
     fn map_x_to_idx(&self, click_x: f32) -> usize {
-        let label_x = self.label_x_offset();
-        let relative_x = click_x - (self.base.x + label_x + 8.0) + self.scroll_x;
+        let label_x = side_offset(&self.label);
+        let relative_x = click_x - (self.rect.x + label_x + 8.0) + self.scroll_x;
         if self.glyph_positions.is_empty() {
             let char_width = self.char_width();
             return ((relative_x / char_width).round() as isize)
                 .max(0)
                 .min(self.edit_buffer.chars().count() as isize) as usize;
         }
-        
+
         let mut closest_idx = 0;
         let mut min_diff = f32::MAX;
         for (i, &pos) in self.glyph_positions.iter().enumerate() {
@@ -120,25 +163,6 @@ impl TextBox {
         closest_idx
     }
 
-    pub fn with_draw_bg_border(mut self, draw: bool) -> Self {
-        self.draw_bg_border = draw;
-        self
-    }
-
-    pub fn with_text_color(mut self, color: Option<[u8; 3]>) -> Self {
-        self.text_color = color;
-        self
-    }
-    pub fn with_font_size(mut self, size: f32) -> Self {
-        self.font_size = size;
-        self
-    }
-
-    pub fn with_font_family(mut self, family: String) -> Self {
-        self.font_family = family;
-        self
-    }
-
     pub fn char_width(&self) -> f32 {
         crate::widget::display::measure_text_width("M", &self.font_family, self.font_size)
     }
@@ -153,7 +177,7 @@ impl TextBox {
         let mut lines = Vec::new();
         let mut current_line = Vec::new();
         let mut index_map = vec![(0, 0); chars.len() + 1];
-        
+
         if !self.line_wrap_enabled() {
             let mut i = 0;
             while i < chars.len() {
@@ -172,13 +196,13 @@ impl TextBox {
             lines.push(current_line.iter().collect::<String>());
             return (lines, index_map);
         }
-        
+
         let max_chars = max_chars_per_line.max(1);
-        
+
         let mut i = 0;
         while i < chars.len() {
             let ch = chars[i];
-            
+
             if ch == '\n' {
                 index_map[i] = (lines.len(), current_line.len());
                 lines.push(current_line.iter().collect::<String>());
@@ -186,10 +210,10 @@ impl TextBox {
                 i += 1;
                 continue;
             }
-            
+
             current_line.push(ch);
             index_map[i] = (lines.len(), current_line.len() - 1);
-            
+
             if current_line.len() > max_chars {
                 let mut space_idx = None;
                 for (s_idx, &c) in current_line.iter().enumerate().rev() {
@@ -198,14 +222,14 @@ impl TextBox {
                         break;
                     }
                 }
-                
+
                 if let Some(s_idx) = space_idx {
                     let line_to_push: Vec<char> = current_line[0..s_idx + 1].to_vec();
                     let remaining: Vec<char> = current_line[s_idx + 1..].to_vec();
-                    
+
                     let line_idx = lines.len();
                     lines.push(line_to_push.iter().collect::<String>());
-                    
+
                     current_line = remaining;
                     let start_orig = i - current_line.len() + 1;
                     for c_idx in 0..current_line.len() {
@@ -214,10 +238,10 @@ impl TextBox {
                 } else {
                     let line_to_push: Vec<char> = current_line[0..max_chars].to_vec();
                     let remaining: Vec<char> = current_line[max_chars..].to_vec();
-                    
+
                     let line_idx = lines.len();
                     lines.push(line_to_push.iter().collect::<String>());
-                    
+
                     current_line = remaining;
                     let start_orig = i - current_line.len() + 1;
                     for c_idx in 0..current_line.len() {
@@ -227,10 +251,10 @@ impl TextBox {
             }
             i += 1;
         }
-        
+
         index_map[chars.len()] = (lines.len(), current_line.len());
         lines.push(current_line.iter().collect::<String>());
-        
+
         (lines, index_map)
     }
 
@@ -238,7 +262,7 @@ impl TextBox {
         let line = target_line.min(max_line_idx);
         let mut best_idx = 0;
         let mut best_dist = usize::MAX;
-        
+
         for (i, &(l, c)) in index_map.iter().enumerate() {
             if l == line {
                 let dist = (c as isize - target_col as isize).abs() as usize;
@@ -251,31 +275,6 @@ impl TextBox {
         best_idx
     }
 
-    pub fn with_password(mut self, is_password: bool) -> Self {
-        self.is_password = is_password;
-        self
-    }
-
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
-    }
-
-    pub fn with_config(mut self, file: &str, key: &str) -> Self {
-        self.base.config_file = Some(file.to_string());
-        self.base.config_key = Some(key.to_string());
-        self
-    }
-
-    pub fn set_label(&mut self, label: &str) {
-        self.base.label = Some(label.to_string());
-    }
-
-    pub fn with_placeholder(mut self, placeholder: &str) -> Self {
-        self.placeholder = Some(placeholder.to_string());
-        self
-    }
-
     fn border_width(&self) -> f32 {
         if self.multiline {
             crate::layout::textbox_multiline_border_width()
@@ -304,20 +303,10 @@ impl TextBox {
         self.multiline && crate::layout::textbox_line_wrap()
     }
 
-    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);
     }
@@ -407,11 +396,33 @@ impl TextBox {
         self.sync_editor_state();
     }
 
+    pub fn set_value(&mut self, val: &str) -> bool {
+        let val_str = val.to_string();
+        if self.text != val_str {
+            self.text = val_str.clone();
+            self.edit_buffer = val_str;
+            self.just_changed = true;
+            let len = self.edit_buffer.chars().count();
+            self.cursor_idx = self.cursor_idx.min(len);
+            if let Some(anchor) = self.select_anchor {
+                self.select_anchor = Some(anchor.min(len));
+            }
+            if self.cursor_idx == 0 && self.select_anchor == Some(0) {
+                self.all_selected = false;
+            }
+            self.sync_editor_state();
+            self.clamp_scroll();
+            true
+        } else {
+            false
+        }
+    }
+
     pub fn clamp_scroll(&mut self) {
         let char_width = self.char_width();
         let line_height = self.line_height();
         let max_chars = if self.line_wrap_enabled() {
-            (((self.base.w - 16.0) / char_width).floor() as usize).max(1)
+            (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
         } else {
             999999
         };
@@ -424,7 +435,7 @@ impl TextBox {
 
         if self.multiline {
             let content_h = lines.len() as f32 * line_height;
-            let max_scroll = (content_h - (self.base.h - 16.0)).max(0.0);
+            let max_scroll = (content_h - (self.rect.height - 16.0)).max(0.0);
             self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
         } else {
             self.scroll_y = 0.0;
@@ -433,7 +444,7 @@ impl TextBox {
         if !self.line_wrap_enabled() {
             let max_line_len = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
             let content_w = max_line_len as f32 * char_width;
-            let max_scroll_x = (content_w - (self.base.w - 16.0)).max(0.0);
+            let max_scroll_x = (content_w - (self.rect.width - 16.0)).max(0.0);
             self.scroll_x = self.scroll_x.clamp(0.0, max_scroll_x);
         } else {
             self.scroll_x = 0.0;
@@ -444,7 +455,7 @@ impl TextBox {
         let char_width = self.char_width();
         let line_height = self.line_height();
         let max_chars = if self.line_wrap_enabled() {
-            (((self.base.w - 16.0) / char_width).floor() as usize).max(1)
+            (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
         } else {
             999999
         };
@@ -459,14 +470,14 @@ impl TextBox {
             (vec![buffer.clone()], m)
         };
         if index_map.is_empty() { return; }
-        
+
         let cursor_idx = self.cursor_idx.min(index_map.len() - 1);
         let (line_idx, col_idx) = index_map[cursor_idx];
-        
-        let top = self.base.label_offset();
-        let viewport_w = self.base.w - 16.0;
-        let viewport_h = self.base.h - top - 16.0;
-        
+
+        let top = self.label_top();
+        let viewport_w = self.rect.width - 16.0;
+        let viewport_h = self.rect.height - top - 16.0;
+
         if self.multiline {
             let line_y = top + 8.0 + (line_idx as f32 * line_height);
             if line_y < self.scroll_y + 10.0 {
@@ -475,7 +486,7 @@ impl TextBox {
                 self.scroll_y = (line_y + line_height - viewport_h + 20.0).max(0.0);
             }
         }
-        
+
         if !self.line_wrap_enabled() {
             let cursor_x = col_idx as f32 * char_width;
             if cursor_x < self.scroll_x + 10.0 {
@@ -486,377 +497,88 @@ impl TextBox {
         }
         self.clamp_scroll();
     }
-}
 
-impl Default for TextBox {
-    fn default() -> Self {
-        Self::new(String::new())
+    /// The legacy `focus()` body minus the global-focus claim (the caller's, via
+    /// `EventCtx::request_focus`).
+    fn begin_editing(&mut self) {
+        if self.disabled { return; }
+        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.sync_editor_state();
     }
-}
-
-impl Element for TextBox {
-    crate::impl_widget_base!(TextBox);
-
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
-        let (style_family, style_size) = crate::layout::control_label_font_detached_parsed();
-        if self.font_size == self.default_font_size {
-            self.font_size = style_size;
-        }
-        self.default_font_size = style_size;
-
-        if self.font_family == self.default_font_family {
-            self.font_family = style_family.clone();
-        }
-        self.default_font_family = style_family;
-
-        let text_src = if self.editing { &self.edit_buffer } else { &self.text };
-        let display_text = if text_src.is_empty() && self.placeholder.is_some() {
-            self.placeholder.as_ref().unwrap().as_str()
-        } else {
-            text_src.as_str()
-        };
-
-        let font_fam = if self.is_password {
-            "monospace"
-        } else {
-            self.font_family.as_str()
-        };
-
-        let render_text = if self.is_password {
-            "•".repeat(display_text.chars().count())
-        } else {
-            display_text.to_string()
-        };
-
-        let buffer = crate::widget::display::text_label::make_widget_text_buffer(fs, &render_text, self.font_size, font_fam);
-
-        let char_count = render_text.chars().count();
-        let mut x_offsets = vec![0.0; char_count + 1];
-        let mut total_w: f32 = 0.0;
-        let scale = crate::scale::scale_factor().max(1.0);
 
-        for run in buffer.layout_runs() {
-            for glyph in run.glyphs {
-                let byte_offset = glyph.start;
-                let c_idx = render_text[..byte_offset.min(render_text.len())].chars().count();
-                if c_idx < x_offsets.len() {
-                    x_offsets[c_idx] = glyph.x / scale;
-                }
-                total_w = total_w.max((glyph.x + glyph.w) / scale);
+    /// The legacy `unfocus()` body: leave edit mode and commit the buffer.
+    fn commit_editing(&mut self) {
+        if self.editing {
+            self.editing = false;
+            if self.text != self.edit_buffer {
+                self.text = self.edit_buffer.clone();
+                self.just_changed = true;
             }
+            self.select_anchor = None;
+            self.all_selected = false;
+            self.sync_editor_state();
         }
+    }
 
-        let mut current_x = 0.0;
-        for i in 0..x_offsets.len() {
-            if x_offsets[i] == 0.0 && i > 0 {
-                x_offsets[i] = current_x;
+    /// Map a press/drag position to a buffer index — the shared body of the legacy
+    /// `mouse_input` press arm and `drag_update`.
+    fn position_to_idx(&self, px: f32, py: f32, with_label_x: bool) -> usize {
+        let char_width = self.char_width();
+        let top = self.label_top();
+        let label_x = if with_label_x { side_offset(&self.label) } else { 0.0 };
+        if self.multiline {
+            let line_height = self.line_height();
+            let max_chars = if self.line_wrap_enabled() {
+                ((((self.rect.width - label_x) - 16.0) / char_width).floor() as usize).max(1)
             } else {
-                current_x = x_offsets[i];
-            }
-        }
-
-        if !x_offsets.is_empty() {
-            let last_idx = x_offsets.len() - 1;
-            x_offsets[last_idx] = total_w;
+                999999
+            };
+            let (lines, index_map) = self.wrap_text(max_chars);
+            let click_line = (((py - (self.rect.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
+            let click_col = (((px - (self.rect.x + label_x + 8.0) + self.scroll_x) / char_width).round() as isize).max(0) as usize;
+            self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
+        } else {
+            self.map_x_to_idx(px)
         }
-
-        self.glyph_positions = x_offsets;
-        self.total_text_width = total_w;
-
-        let cursor_pos = self.cursor_idx.min(self.glyph_positions.len() - 1);
-        self.cursor_x_offset = self.glyph_positions.get(cursor_pos).copied().unwrap_or(0.0);
-
-    }
-
-    fn get_value_string(&self) -> Option<String> {
-        Some(self.text.clone())
     }
 
-    fn set_value_string(&mut self, val: &str) -> bool {
-        let val_str = val.to_string();
-        if self.text != val_str {
-            self.text = val_str.clone();
-            self.edit_buffer = val_str;
-            self.just_changed = true;
+    /// Extend the selection to a drag position — the shared body of the legacy
+    /// `on_cursor_moved` drag arm and `drag_update` (which used no label inset).
+    fn extend_selection_to(&mut self, px: f32, py: f32) -> bool {
+        let drag_idx = self.position_to_idx(px, py, false);
+        if self.cursor_idx != drag_idx {
+            self.cursor_idx = drag_idx;
+            self.just_focused = false;
             let len = self.edit_buffer.chars().count();
-            self.cursor_idx = self.cursor_idx.min(len);
-            if let Some(anchor) = self.select_anchor {
-                self.select_anchor = Some(anchor.min(len));
-            }
-            if self.cursor_idx == 0 && self.select_anchor == Some(0) {
-                self.all_selected = false;
-            }
-            self.sync_editor_state();
-            self.clamp_scroll();
+            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;
             true
         } else {
             false
         }
     }
 
-    fn take_change(&mut self) -> bool {
-        self.take_change()
-    }
-
-    fn cut_selection(&mut self) -> bool {
-        let res = self.cut_selection();
-        if res {
-            self.just_changed = true;
-        }
-        res
-    }
-
-    fn copy_selection(&self) {
-        self.copy_selection();
-    }
+    /// Port of the legacy `keyboard_input` body.
+    fn handle_key(&mut self, event: &KeyEvent) -> bool {
+        if !self.editing || self.disabled { return false; }
+        if event.state != ElementState::Pressed { return false; }
 
-    fn paste_from_clipboard(&mut self) -> bool {
-        let res = self.paste_from_clipboard();
-        if res {
-            self.just_changed = true;
-        }
-        res
-    }
+        let control = event.ctrl;
 
-    fn select_all(&mut self) {
-        self.select_all();
-    }
+        let mut state = TextEditorState {
+            buffer: self.edit_buffer.clone(),
+            cursor_idx: self.cursor_idx,
+            select_anchor: self.select_anchor,
+            all_selected: self.all_selected,
+        };
 
-    fn clear_text(&mut self) {
-        self.set_value_string("");
-    }
-
-    fn preferred_height(&self) -> Option<f32> {
-        Some(crate::layout::textbox_height())
-    }
-
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        let r = crate::layout::textbox_corner_radius();
-        if r > 0.0 {
-            (true, true, true, true)
-        } else {
-            (false, false, false, false)
-        }
-    }
-
-    fn corner_radius(&self) -> f32 {
-        crate::layout::textbox_corner_radius()
-    }
-
-    fn rect(&self) -> (f32, f32, f32, f32) { (self.base.x, self.base.y, self.base.w, self.base.h) }
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        let final_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
-        };
-        if let Some(b) = self.base_mut() {
-            b.x = x;
-            b.y = y;
-            b.w = final_w;
-            b.h = h;
-        }
-        self.clamp_scroll();
-    }
-    fn set_row_rect(&mut self, x: f32, w: f32) {
-        let final_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
-        };
-        if let Some(b) = self.base_mut() {
-            b.row_x = x;
-            b.row_w = final_w;
-        }
-    }
-
-    fn color(&self) -> [f32; 4] {
-        [0.10, 0.10, 0.16, 1.0]
-    }
-
-    fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if self.disabled {
-            let was = self.base.hovered;
-            self.base.hovered = false;
-            return was;
-        }
-        let mut changed = false;
-        if self.dragging && self.editing {
-            let char_width = self.char_width();
-            let drag_idx = if self.multiline {
-                let line_height = self.line_height();
-                let max_chars = if self.line_wrap_enabled() {
-                    (((self.base.w - 16.0) / char_width).floor() as usize).max(1)
-                } else {
-                    999999
-                };
-                let (lines, index_map) = self.wrap_text(max_chars);
-                let top = self.base.label_offset();
-                let click_line = (((py - (self.base.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
-                let click_col = (((px - (self.base.x + 8.0) + self.scroll_x) / char_width).round() as isize).max(0) as usize;
-                self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
-            } else {
-                self.map_x_to_idx(px)
-            };
-            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.base.hovered;
-        self.base.hovered = self.hit_test(px, py, ctx);
-        if was != self.base.hovered {
-            changed = true;
-        }
-        changed
-    }
-
-    fn draggable(&self) -> bool { !self.disabled }
-    fn is_dragging(&self) -> bool { self.dragging }
-    fn widget_font(&self) -> Option<String> { Some(crate::layout::control_label_font_detached()) }
-
-    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 = self.char_width();
-        let top = self.base.label_offset();
-        let drag_idx = if self.multiline {
-            let line_height = self.line_height();
-            let max_chars = if self.line_wrap_enabled() {
-                (((self.base.w - 16.0) / char_width).floor() as usize).max(1)
-            } else {
-                999999
-            };
-            let (lines, index_map) = self.wrap_text(max_chars);
-            let click_line = (((py - (self.base.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
-            let click_col = (((px - (self.base.x + 8.0) + self.scroll_x) / char_width).round() as isize).max(0) as usize;
-            self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
-        } else {
-            self.map_x_to_idx(px)
-        };
-        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 value(&self) -> i32 { 0 }
-
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if self.disabled { return false; }
-        if button == MouseButton::Right && state == ElementState::Pressed {
-            if self.hit_test(px, py, ctx) {
-                if !self.editing {
-                    self.focus();
-                }
-                ctx.handle_right_click(self.as_ptr_mut(), px, py);
-                return true;
-            }
-        }
-        if button != MouseButton::Left { return false; }
-        if !self.hit_test(px, py, ctx) { return false; }
-        match state {
-            ElementState::Pressed => {
-                if !self.editing {
-                    self.focus();
-                } else {
-                    let char_width = self.char_width();
-                    let top = self.base.label_offset();
-                    let label_x = self.label_x_offset();
-                    let idx = if self.multiline {
-                        let line_height = self.line_height();
-                        let max_chars = if self.line_wrap_enabled() {
-                            ((((self.base.w - label_x) - 16.0) / char_width).floor() as usize).max(1)
-                        } else {
-                            999999
-                        };
-                        let (lines, index_map) = self.wrap_text(max_chars);
-                        let click_line = (((py - (self.base.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
-                        let click_col = (((px - (self.base.x + label_x + 8.0) + self.scroll_x) / char_width).round() as isize).max(0) as usize;
-                        self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
-                    } else {
-                        self.map_x_to_idx(px)
-                    };
-                    self.cursor_idx = idx;
-                    self.select_anchor = Some(idx);
-                    self.all_selected = false;
-                }
-                true
-            }
-            ElementState::Released => {
-                if self.dragging {
-                    self.drag_end();
-                }
-                if self.select_anchor == Some(self.cursor_idx) {
-                    self.select_anchor = None;
-                }
-                true
-            }
-        }
-    }
-
-    fn focus(&mut self) {
-        if self.disabled { return; }
-        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.sync_editor_state();
-        focus::set_focused(self);
-    }
-
-    fn unfocus(&mut self) {
-        if self.editing {
-            self.editing = false;
-            if self.text != self.edit_buffer {
-                self.text = self.edit_buffer.clone();
-                self.just_changed = true;
-            }
-            self.select_anchor = None;
-            self.all_selected = false;
-            self.sync_editor_state();
-        }
-    }
-
-    fn keyboard_input(&mut self, event: &KeyEvent, _ctx: &mut UiContext) -> bool {
-        if !self.editing || self.disabled { return false; }
-        if event.state != ElementState::Pressed { return false; }
-        
-        let control = event.ctrl;
-        
-        let mut state = TextEditorState {
-            buffer: self.edit_buffer.clone(),
-            cursor_idx: self.cursor_idx,
-            select_anchor: self.select_anchor,
-            all_selected: self.all_selected,
-        };
-        
         let handled = match &event.logical_key {
             Key::Named(NamedKey::Backspace) => {
                 state.delete_backwards()
@@ -882,7 +604,7 @@ impl Element for TextBox {
                 }
                 if self.multiline {
                     let char_width = self.char_width();
-                    let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
+                    let max_chars = (((self.rect.width - 16.0) / char_width).floor() as usize).max(1);
                     let (lines, index_map) = self.wrap_text(max_chars);
                     let (cursor_l, cursor_c) = index_map[state.cursor_idx.min(index_map.len() - 1)];
                     if cursor_l > 0 {
@@ -907,7 +629,7 @@ impl Element for TextBox {
                 }
                 if self.multiline {
                     let char_width = self.char_width();
-                    let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
+                    let max_chars = (((self.rect.width - 16.0) / char_width).floor() as usize).max(1);
                     let (lines, index_map) = self.wrap_text(max_chars);
                     let (cursor_l, cursor_c) = index_map[state.cursor_idx.min(index_map.len() - 1)];
                     if cursor_l < lines.len() - 1 {
@@ -931,7 +653,7 @@ impl Element for TextBox {
                     state.insert_text("\n");
                     true
                 } else {
-                    self.unfocus();
+                    self.commit_editing();
                     true
                 }
             }
@@ -985,7 +707,7 @@ impl Element for TextBox {
                 }
             }
         };
-        
+
         if self.editing {
             self.edit_buffer = state.buffer;
             self.cursor_idx = state.cursor_idx;
@@ -994,509 +716,750 @@ impl Element for TextBox {
             self.sync_editor_state();
             self.scroll_to_cursor();
         }
-        
+
         handled
     }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        let (r1, r2, r3, r4) = self.rounded_corners();
-        let has_rounded = r1 || r2 || r3 || r4;
-        if has_rounded {
-            return quads;
-        }
-        
-        let top = self.base.label_offset();
-        let visual_h = self.base.h - top;
-        let border_w = self.border_width();
-        if self.disabled {
-            if self.draw_bg_border {
-                quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, [0.12, 0.12, 0.16, 1.0]));
-                quads.push((self.base.x + border_w, self.base.y + top + border_w, self.base.w - 2.0 * border_w, visual_h - 2.0 * border_w, [0.06, 0.06, 0.08, 1.0]));
+    /// Port of the legacy `mouse_wheel` body (scroll the multiline/no-wrap viewports).
+    fn handle_wheel(&mut self, delta: &MouseScrollDelta) -> bool {
+        if self.disabled { return false; }
+        let char_width = self.char_width();
+        let line_height = self.line_height();
+
+        let max_chars = if self.line_wrap_enabled() {
+            (((self.rect.width - 16.0) / char_width).floor() as usize).max(1)
+        } else {
+            999999
+        };
+
+        let (lines, _) = if self.multiline {
+            self.wrap_text(max_chars)
+        } else {
+            let buffer = if self.editing { &self.edit_buffer } else { &self.text };
+            (vec![buffer.clone()], vec![(0, 0); buffer.chars().count() + 1])
+        };
+
+        let mut changed = false;
+
+        if self.multiline {
+            let content_h = lines.len() as f32 * line_height;
+            let max_scroll = (content_h - (self.rect.height - 16.0)).max(0.0);
+            let scroll_amt = match *delta {
+                MouseScrollDelta::LineDelta(_, dy) => -dy * line_height * 2.0,
+                MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
+            };
+            let old_scroll = self.scroll_y;
+            self.scroll_y = (self.scroll_y + scroll_amt).clamp(0.0, max_scroll);
+            if old_scroll != self.scroll_y {
+                changed = true;
             }
-            return quads;
         }
-        
-        if self.draw_bg_border {
-            let bg_color = if self.editing {
-                crate::colors::textbox_background_edit_color()
-            } else {
-                crate::colors::textbox_background_color()
-            };
-            let border_color = if self.editing {
-                [0.20, 0.50, 0.85, 1.0]
-            } else if self.base.hovered {
-                [0.25, 0.25, 0.35, 1.0]
-            } else {
-                [0.18, 0.18, 0.24, 1.0]
+
+        if !self.line_wrap_enabled() {
+            let max_line_len = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
+            let content_w = max_line_len as f32 * char_width;
+            let max_scroll_x = (content_w - (self.rect.width - 16.0)).max(0.0);
+            let natural = crate::layout::touchpad_natural_scroll();
+            let scroll_amt_x = match *delta {
+                MouseScrollDelta::LineDelta(dx, dy) => {
+                    if !self.multiline {
+                        let scroll_val = if dy != 0.0 { -dy } else { if natural { -dx } else { dx } };
+                        scroll_val * char_width * 3.0
+                    } else {
+                        let scroll_val = if natural { -dx } else { dx };
+                        scroll_val * char_width * 3.0
+                    }
+                }
+                MouseScrollDelta::PixelDelta(pos) => {
+                    if !self.multiline {
+                        let scroll_val = if pos.y != 0.0 { -pos.y as f32 } else { if natural { -pos.x as f32 } else { pos.x as f32 } };
+                        scroll_val
+                    } else {
+                        if natural { -pos.x as f32 } else { pos.x as f32 }
+                    }
+                }
             };
-            quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, border_color));
-            quads.push((self.base.x + border_w, self.base.y + top + border_w, self.base.w - 2.0 * border_w, visual_h - 2.0 * border_w, bg_color));
+            let old_scroll_x = self.scroll_x;
+            self.scroll_x = (self.scroll_x + scroll_amt_x).clamp(0.0, max_scroll_x);
+            if old_scroll_x != self.scroll_x {
+                changed = true;
+            }
         }
 
-        if self.editing || self.select_anchor.is_some() {
-            let char_width = self.char_width();
-            let line_height = self.line_height();
-            
-            let highlight_color = [0.20, 0.50, 0.85, 0.3];
-            let cursor_color = if self.draw_bg_border {
-                [0.80, 0.80, 0.85, 1.0]
-            } else {
-                [0.10, 0.10, 0.15, 1.0]
-            };
+        changed
+    }
 
-            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);
+    /// Selection highlight + caret quads, shared by both render branches. `x`/`w` are the
+    /// (possibly label-inset) horizontal span the branch draws in — the legacy paths differed
+    /// (non-rounded used the full base span, rounded inset by the side label).
+    fn selection_quads(&self, x: f32, w: f32, out: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) {
+        if !(self.editing || self.select_anchor.is_some()) {
+            return;
+        }
+        let top = self.label_top();
+        let char_width = self.char_width();
+        let line_height = self.line_height();
 
-            if self.multiline {
-                let max_chars = if self.line_wrap_enabled() {
-                    (((self.base.w - 16.0) / char_width).floor() as usize).max(1)
-                } else {
-                    999999
-                };
-                let (_lines, index_map) = self.wrap_text(max_chars);
-
-                let view_top = self.base.y + top;
-                let view_bottom = self.base.y + self.base.h;
-
-                if start != end {
-                    let start_pos = index_map[start.min(index_map.len() - 1)];
-                    let end_pos = index_map[end.min(index_map.len() - 1)];
-                    
-                    for line_idx in start_pos.0..=end_pos.0 {
-                        let mut line_start_col = None;
-                        let mut line_end_col = None;
-                        for idx in start..end {
-                            if idx < index_map.len() {
-                                let (l, c) = index_map[idx];
-                                if l == line_idx {
-                                    if line_start_col.is_none() || c < line_start_col.unwrap() {
-                                        line_start_col = Some(c);
-                                    }
-                                    if line_end_col.is_none() || c > line_end_col.unwrap() {
-                                        line_end_col = Some(c);
-                                    }
+        let highlight_color = [0.20, 0.50, 0.85, 0.3];
+        let cursor_color = if self.draw_bg_border {
+            [0.80, 0.80, 0.85, 1.0]
+        } else {
+            [0.10, 0.10, 0.15, 1.0]
+        };
+
+        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 self.multiline {
+            let max_chars = if self.line_wrap_enabled() {
+                (((w - 16.0) / char_width).floor() as usize).max(1)
+            } else {
+                999999
+            };
+            let (_lines, index_map) = self.wrap_text(max_chars);
+
+            let view_top = self.rect.y + top;
+            let view_bottom = self.rect.y + self.rect.height;
+
+            if start != end {
+                let start_pos = index_map[start.min(index_map.len() - 1)];
+                let end_pos = index_map[end.min(index_map.len() - 1)];
+
+                for line_idx in start_pos.0..=end_pos.0 {
+                    let mut line_start_col = None;
+                    let mut line_end_col = None;
+                    for idx in start..end {
+                        if idx < index_map.len() {
+                            let (l, c) = index_map[idx];
+                            if l == line_idx {
+                                if line_start_col.is_none() || c < line_start_col.unwrap() {
+                                    line_start_col = Some(c);
+                                }
+                                if line_end_col.is_none() || c > line_end_col.unwrap() {
+                                    line_end_col = Some(c);
                                 }
                             }
                         }
-                        if let (Some(sc), Some(ec)) = (line_start_col, line_end_col) {
-                            let highlight_x = self.base.x + 8.0 + (sc as f32 * char_width) - self.scroll_x;
-                            let highlight_w = (ec - sc + 1) as f32 * char_width;
-                            let highlight_y = self.base.y + top + 8.0 + (line_idx as f32 * line_height) - self.scroll_y;
-                            let clipped_y = highlight_y.max(view_top);
-                            let clipped_bottom = (highlight_y + line_height).min(view_bottom);
-                            let h_left = highlight_x.max(self.base.x + 8.0);
-                            let h_right = (highlight_x + highlight_w).min(self.base.x + self.base.w - 8.0);
-                            if h_left < h_right && clipped_y < clipped_bottom {
-                                quads.push((
-                                    h_left,
-                                    clipped_y,
-                                    h_right - h_left,
-                                    clipped_bottom - clipped_y,
-                                    highlight_color,
-                                ));
-                            }
+                    }
+                    if let (Some(sc), Some(ec)) = (line_start_col, line_end_col) {
+                        let highlight_x = x + 8.0 + (sc as f32 * char_width) - self.scroll_x;
+                        let highlight_w = (ec - sc + 1) as f32 * char_width;
+                        let highlight_y = self.rect.y + top + 8.0 + (line_idx as f32 * line_height) - self.scroll_y;
+                        let clipped_y = highlight_y.max(view_top);
+                        let clipped_bottom = (highlight_y + line_height).min(view_bottom);
+                        let h_left = highlight_x.max(x + 8.0);
+                        let h_right = (highlight_x + highlight_w).min(x + w - 8.0);
+                        if h_left < h_right && clipped_y < clipped_bottom {
+                            out.push((h_left, clipped_y, h_right - h_left, clipped_bottom - clipped_y, highlight_color));
                         }
                     }
                 }
-                
-                if self.editing {
-                    let caret_h = self.font_size * 1.15;
-                    let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
-                    let cursor_x = self.base.x + 8.0 + (cursor_c as f32 * char_width) - self.scroll_x;
-                    let cursor_y = self.base.y + top + 8.0 + (cursor_l as f32 * line_height) + (line_height - caret_h) / 2.0 - self.scroll_y;
-                    let clipped_y = cursor_y.max(view_top);
-                    let clipped_bottom = (cursor_y + caret_h).min(view_bottom);
-                    if cursor_x >= self.base.x + 8.0 && cursor_x <= self.base.x + self.base.w - 8.0 {
-                        if clipped_y < clipped_bottom {
-                            quads.push((cursor_x, clipped_y, 1.5, clipped_bottom - clipped_y, cursor_color));
-                        }
+            }
+
+            if self.editing {
+                let caret_h = self.font_size * 1.15;
+                let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
+                let cursor_x = x + 8.0 + (cursor_c as f32 * char_width) - self.scroll_x;
+                let cursor_y = self.rect.y + top + 8.0 + (cursor_l as f32 * line_height) + (line_height - caret_h) / 2.0 - self.scroll_y;
+                let clipped_y = cursor_y.max(view_top);
+                let clipped_bottom = (cursor_y + caret_h).min(view_bottom);
+                if cursor_x >= x + 8.0 && cursor_x <= x + w - 8.0 {
+                    if clipped_y < clipped_bottom {
+                        out.push((cursor_x, clipped_y, 1.5, clipped_bottom - clipped_y, cursor_color));
                     }
                 }
+            }
+        } else {
+            let caret_h = self.font_size * 1.15;
+            if start != end {
+                let h_left_offset = self.glyph_positions.get(start).copied().unwrap_or_else(|| start as f32 * char_width);
+                let h_right_offset = self.glyph_positions.get(end).copied().unwrap_or_else(|| end as f32 * char_width);
+                let highlight_x = x + 8.0 + h_left_offset - self.scroll_x;
+                let h_left = highlight_x.max(x + 8.0);
+                let h_right = (x + 8.0 + h_right_offset - self.scroll_x).min(x + w - 8.0);
+                if h_left < h_right {
+                    out.push((
+                        h_left,
+                        crate::layout::align_text_y(self.rect.y, self.rect.height, self.font_size, top),
+                        h_right - h_left,
+                        crate::layout::line_height(self.font_size),
+                        highlight_color,
+                    ));
+                }
+            }
+
+            if self.editing {
+                let offset = if self.glyph_positions.is_empty() {
+                    self.cursor_idx as f32 * char_width
+                } else {
+                    self.cursor_x_offset
+                };
+                let cursor_x = x + 8.0 + offset - self.scroll_x;
+                if cursor_x >= x + 8.0 && cursor_x <= x + w - 8.0 {
+                    let text_y = crate::layout::align_text_y(self.rect.y, self.rect.height, self.font_size, top);
+                    let cursor_y = text_y + (self.font_size - caret_h) / 2.0;
+                    out.push((cursor_x, cursor_y, 1.5, caret_h, cursor_color));
+                }
+            }
+        }
+    }
+
+    /// The value/placeholder text lines — the legacy `text_labels` body minus the control
+    /// label (the adapter's base-label machinery draws that).
+    fn value_labels(&self) -> Vec<TextLabel> {
+        let mut labels = Vec::new();
+        let top = self.label_top();
+        let mut val_text = if self.editing {
+            self.edit_buffer.clone()
+        } else {
+            self.text.clone()
+        };
+        if self.is_password {
+            val_text = "•".repeat(val_text.chars().count());
+        }
+
+        let is_placeholder = val_text.is_empty() && self.placeholder.is_some();
+        let display_text = if is_placeholder {
+            self.placeholder.as_ref().unwrap().clone()
+        } else {
+            val_text
+        };
+
+        let label_color = if is_placeholder {
+            crate::colors::textbox_placeholder_text_color()
+        } else if let Some(custom_color) = self.text_color {
+            custom_color
+        } else 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]
+        };
+
+        let label_x = side_offset(&self.label);
+        let x = self.rect.x + label_x;
+        let w = self.rect.width - label_x;
+
+        if self.multiline {
+            let char_width = self.char_width();
+            let line_height = self.line_height();
+            let max_chars = if self.line_wrap_enabled() {
+                (((w - 16.0) / char_width).floor() as usize).max(1)
             } else {
-                let caret_h = self.font_size * 1.15;
-                if start != end {
-                    let h_left_offset = self.glyph_positions.get(start).copied().unwrap_or_else(|| start as f32 * char_width);
-                    let h_right_offset = self.glyph_positions.get(end).copied().unwrap_or_else(|| end as f32 * char_width);
-                    let highlight_x = self.base.x + 8.0 + h_left_offset - self.scroll_x;
-                    let h_left = highlight_x.max(self.base.x + 8.0);
-                    let h_right = (self.base.x + 8.0 + h_right_offset - self.scroll_x).min(self.base.x + self.base.w - 8.0);
-                    if h_left < h_right {
-                        quads.push((
-                            h_left,
-                            crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, top),
-                            h_right - h_left,
-                            crate::layout::line_height(self.font_size),
-                            highlight_color,
-                        ));
+                999999
+            };
+            let (lines, _) = self.wrap_text(max_chars);
+            let lines_to_draw = if is_placeholder {
+                let placeholder_src = self.placeholder.as_ref().unwrap();
+                let chars: Vec<char> = placeholder_src.chars().collect();
+                let mut p_lines = Vec::new();
+                let mut current_line = Vec::new();
+                for ch in chars {
+                    if ch == '\n' {
+                        p_lines.push(current_line.iter().collect::<String>());
+                        current_line.clear();
+                    } else {
+                        current_line.push(ch);
+                        if self.line_wrap_enabled() && current_line.len() > max_chars {
+                            p_lines.push(current_line.iter().collect::<String>());
+                            current_line.clear();
+                        }
                     }
                 }
+                p_lines.push(current_line.iter().collect::<String>());
+                p_lines
+            } else {
+                lines
+            };
+            for (line_idx, line_text) in lines_to_draw.iter().enumerate() {
+                labels.push(TextLabel {
+                    text: line_text.clone(),
+                    x: x + 8.0 - self.scroll_x,
+                    y: self.rect.y + top + 8.0 + (line_idx as f32 * line_height) + (line_height - self.font_size) / 2.0 - self.scroll_y,
+                    font_size: self.font_size,
+                    color: label_color,
+                });
+            }
+        } else {
+            labels.push(TextLabel {
+                text: display_text,
+                x: x + 8.0 - self.scroll_x,
+                y: crate::layout::align_text_y(self.rect.y, self.rect.height, self.font_size, top),
+                font_size: self.font_size,
+                color: label_color,
+            });
+        }
+        labels
+    }
+}
+
+impl Adapted<TextBox> {
+    pub fn with_update_on_type(mut self, update: bool) -> Self {
+        self.update_on_type = update;
+        self
+    }
+
+    pub fn with_multiline(mut self, multiline: bool) -> Self {
+        self.multiline = multiline;
+        self
+    }
+
+    pub fn with_draw_bg_border(mut self, draw: bool) -> Self {
+        self.draw_bg_border = draw;
+        self
+    }
+
+    pub fn with_text_color(mut self, color: Option<[u8; 3]>) -> Self {
+        self.text_color = color;
+        self
+    }
+
+    pub fn with_font_size(mut self, size: f32) -> Self {
+        self.font_size = size;
+        self
+    }
+
+    pub fn with_font_family(mut self, family: String) -> Self {
+        self.font_family = family;
+        self
+    }
+
+    pub fn with_password(mut self, is_password: bool) -> Self {
+        self.is_password = is_password;
+        self
+    }
+
+    pub fn with_placeholder(mut self, placeholder: &str) -> Self {
+        self.placeholder = Some(placeholder.to_string());
+        self
+    }
+
+    pub fn with_max_width(mut self, max_w: Option<f32>) -> Self {
+        self.max_width = max_w;
+        self
+    }
 
-                if self.editing {
-                    let offset = if self.glyph_positions.is_empty() {
-                        self.cursor_idx as f32 * char_width
+    pub fn with_width(mut self, w: f32) -> Self {
+        self.width = Some(w);
+        self
+    }
+}
+
+impl Layout for TextBox {
+    fn inflates_label_rect(&self) -> bool {
+        false
+    }
+
+    fn detached_label_inset(&self) -> f32 {
+        4.0
+    }
+
+    fn intrinsic_size(&self) -> Option<Size> {
+        Some(Size::new(0.0, crate::layout::textbox_height()))
+    }
+
+    fn hit_row_rect(&self) -> bool {
+        true
+    }
+
+    /// The legacy `set_rect` width clamp: an explicit `width` wins, else cap at `max_width`.
+    fn adjust_rect(&self, requested: Rect) -> Rect {
+        let final_w = if let Some(explicit_w) = self.width {
+            explicit_w
+        } else if let Some(max_w) = self.max_width {
+            requested.width.min(max_w)
+        } else {
+            requested.width
+        };
+        Rect { width: final_w, ..requested }
+    }
+
+    /// The legacy `set_row_rect` applied the same clamp to the row span.
+    fn adjust_row_rect(&self, x: f32, w: f32) -> (f32, f32) {
+        let final_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
+        };
+        (x, final_w)
+    }
+
+    fn rect_assigned(&mut self, rect: Rect) {
+        self.rect = rect;
+        self.clamp_scroll();
+    }
+}
+
+impl Paint for TextBox {
+    fn color(&self) -> [f32; 4] {
+        [0.10, 0.10, 0.16, 1.0]
+    }
+
+    fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+        let r = crate::layout::textbox_corner_radius();
+        if r > 0.0 {
+            Some((r, (true, true, true, true)))
+        } else {
+            Some((r, (false, false, false, false)))
+        }
+    }
+
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::control_label_font_detached())
+    }
+
+    fn sync_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
+    }
+
+    /// Legacy TextBox kept the shared focus-highlight overlay (the focused editor's
+    /// primary-tint wash — data-editor's teal editing surface).
+    fn legacy_focus_highlight(&self) -> bool {
+        true
+    }
+
+    fn text_bounds(&self, rect: Rect) -> Option<[f32; 4]> {
+        // Legacy bounded-text getters clipped to the full base rect, inset on the left by the
+        // side label.
+        let top = self.label_top();
+        let base_y = rect.y - top;
+        let base_h = rect.height + top;
+        let label_x = side_offset(&self.label);
+        Some([rect.x + label_x, base_y, rect.x + rect.width, base_y + base_h])
+    }
+
+    /// The legacy `prepare_text`: sync font family/size with the live config defaults, then
+    /// shape the display text and record per-glyph advances (`map_x_to_idx` reads them).
+    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
+        let (style_family, style_size) = crate::layout::control_label_font_detached_parsed();
+        if self.font_size == self.default_font_size {
+            self.font_size = style_size;
+        }
+        self.default_font_size = style_size;
+
+        if self.font_family == self.default_font_family {
+            self.font_family = style_family.clone();
+        }
+        self.default_font_family = style_family;
+
+        let text_src = if self.editing { &self.edit_buffer } else { &self.text };
+        let display_text = if text_src.is_empty() && self.placeholder.is_some() {
+            self.placeholder.as_ref().unwrap().as_str()
+        } else {
+            text_src.as_str()
+        };
+
+        let font_fam = if self.is_password {
+            "monospace"
+        } else {
+            self.font_family.as_str()
+        };
+
+        let render_text = if self.is_password {
+            "•".repeat(display_text.chars().count())
+        } else {
+            display_text.to_string()
+        };
+
+        let buffer = crate::widget::display::text_label::make_widget_text_buffer(fs, &render_text, self.font_size, font_fam);
+
+        let char_count = render_text.chars().count();
+        let mut x_offsets = vec![0.0; char_count + 1];
+        let mut total_w: f32 = 0.0;
+        let scale = crate::scale::scale_factor().max(1.0);
+
+        for run in buffer.layout_runs() {
+            for glyph in run.glyphs {
+                let byte_offset = glyph.start;
+                let c_idx = render_text[..byte_offset.min(render_text.len())].chars().count();
+                if c_idx < x_offsets.len() {
+                    x_offsets[c_idx] = glyph.x / scale;
+                }
+                total_w = total_w.max((glyph.x + glyph.w) / scale);
+            }
+        }
+
+        let mut current_x = 0.0;
+        for i in 0..x_offsets.len() {
+            if x_offsets[i] == 0.0 && i > 0 {
+                x_offsets[i] = current_x;
+            } else {
+                current_x = x_offsets[i];
+            }
+        }
+
+        if !x_offsets.is_empty() {
+            let last_idx = x_offsets.len() - 1;
+            x_offsets[last_idx] = total_w;
+        }
+
+        self.glyph_positions = x_offsets;
+        self.total_text_width = total_w;
+
+        let cursor_pos = self.cursor_idx.min(self.glyph_positions.len() - 1);
+        self.cursor_x_offset = self.glyph_positions.get(cursor_pos).copied().unwrap_or(0.0);
+    }
+
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let top = self.label_top();
+        let base_y = rect.y - top;
+        let base_h = rect.height + top;
+        let visual_h = rect.height;
+        let radius = crate::layout::textbox_corner_radius();
+        let border_w = self.border_width();
+
+        // Keep the model's cached rect and the paint rect consistent: paint receives the
+        // content rect derived from the same base the cache holds, so the bodies below read
+        // `self.rect` (the legacy `self.base`) exactly as legacy did. `rect` is used only to
+        // localize this frame's geometry.
+        let _ = (base_y, base_h);
+
+        if radius <= 0.0 {
+            // Legacy `extra_quads`: full base span (no side-label inset), disabled
+            // special-case with early return.
+            let mut quads: Vec<(f32, f32, f32, f32, [f32; 4])> = Vec::new();
+            if self.disabled {
+                if self.draw_bg_border {
+                    quads.push((self.rect.x, self.rect.y + top, self.rect.width, visual_h, [0.12, 0.12, 0.16, 1.0]));
+                    quads.push((self.rect.x + border_w, self.rect.y + top + border_w, self.rect.width - 2.0 * border_w, visual_h - 2.0 * border_w, [0.06, 0.06, 0.08, 1.0]));
+                }
+            } else {
+                if self.draw_bg_border {
+                    let bg_color = if self.editing {
+                        crate::colors::textbox_background_edit_color()
                     } else {
-                        self.cursor_x_offset
+                        crate::colors::textbox_background_color()
                     };
-                    let cursor_x = self.base.x + 8.0 + offset - self.scroll_x;
-                    if cursor_x >= self.base.x + 8.0 && cursor_x <= self.base.x + self.base.w - 8.0 {
-                        let text_y = crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, top);
-                        let cursor_y = text_y + (self.font_size - caret_h) / 2.0;
-                        quads.push((cursor_x, cursor_y, 1.5, caret_h, cursor_color));
-                    }
+                    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.rect.x, self.rect.y + top, self.rect.width, visual_h, border_color));
+                    quads.push((self.rect.x + border_w, self.rect.y + top + border_w, self.rect.width - 2.0 * border_w, visual_h - 2.0 * border_w, bg_color));
                 }
+                self.selection_quads(self.rect.x, self.rect.width, &mut quads);
             }
-        }
+            for (qx, qy, qw, qh, qc) in quads {
+                ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+            }
+        } else {
+            // Legacy `all_rounded_quads`: side-label inset, no disabled special-case.
+            let label_x = side_offset(&self.label);
+            let x = self.rect.x + label_x;
+            let w = self.rect.width - label_x;
 
-        quads
-    }
+            let bg_color = if self.editing {
+                crate::colors::textbox_background_edit_color()
+            } else {
+                crate::colors::textbox_background_color()
+            };
+            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]
+            };
 
-    fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
-        let mut quads = Vec::new();
-        let (r1, r2, r3, r4) = self.rounded_corners();
-        let has_rounded = r1 || r2 || r3 || r4;
-        if !has_rounded {
-            for &child_ptr in &self.children(ctx) {
-                let widget = unsafe { &*child_ptr };
-                quads.extend(widget.all_rounded_quads(ctx));
+            if self.draw_bg_border {
+                let corners = (true, true, true, true);
+                ctx.rounded_rect(Rect { x, y: self.rect.y + top, width: w, height: visual_h }, radius, corners, border_color);
+                ctx.rounded_rect(
+                    Rect { x: x + border_w, y: self.rect.y + top + border_w, width: w - 2.0 * border_w, height: visual_h - 2.0 * border_w },
+                    (radius - border_w).max(0.0),
+                    corners,
+                    bg_color,
+                );
             }
-            return quads;
-        }
 
-        let top = self.base.label_offset();
-        let visual_h = self.base.h - top;
-        let radius = self.corner_radius();
-        
-        let bg_color = if self.editing {
-            crate::colors::textbox_background_edit_color()
-        } else {
-            crate::colors::textbox_background_color()
-        };
-        let border_color = if self.editing {
-            [0.20, 0.50, 0.85, 1.0]
-        } else if self.base.hovered {
-            [0.25, 0.25, 0.35, 1.0]
-        } else {
-            [0.18, 0.18, 0.24, 1.0]
-        };
-        
-        let label_x = self.label_x_offset();
-        let x = self.base.x + label_x;
-        let w = self.base.w - label_x;
-
-        if self.draw_bg_border {
-            let border_w = self.border_width();
-            quads.push((x, self.base.y + top, w, visual_h, radius, border_color, (r1, r2, r3, r4)));
-            quads.push((x + border_w, self.base.y + top + border_w, w - 2.0 * border_w, visual_h - 2.0 * border_w, (radius - border_w).max(0.0), bg_color, (r1, r2, r3, r4)));
+            let mut quads: Vec<(f32, f32, f32, f32, [f32; 4])> = Vec::new();
+            self.selection_quads(x, w, &mut quads);
+            for (qx, qy, qw, qh, qc) in quads {
+                ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+            }
         }
 
-        if self.editing || self.select_anchor.is_some() {
-            let char_width = self.char_width();
-            let line_height = self.line_height();
-            
-            let highlight_color = [0.20, 0.50, 0.85, 0.3];
-            let cursor_color = if self.draw_bg_border {
-                [0.80, 0.80, 0.85, 1.0]
-            } else {
-                [0.10, 0.10, 0.15, 1.0]
-            };
+        for tl in self.value_labels() {
+            ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+        }
+    }
+}
 
-            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);
+impl Input for TextBox {
+    fn tracks_base_focus(&self) -> bool {
+        false
+    }
 
-            if self.multiline {
-                let max_chars = if self.line_wrap_enabled() {
-                    (((w - 16.0) / char_width).floor() as usize).max(1)
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        match event {
+            Event::MouseButton { button: MouseButton::Right, state: ElementState::Pressed, x: px, y: py, .. } => {
+                if self.disabled { return false; }
+                // Adapter hit-gates presses; legacy focused an un-editing box before opening
+                // the menu (work-before-menu, so `opens_context_menu` can't express it).
+                if !self.editing {
+                    self.begin_editing();
+                    ectx.request_focus();
+                }
+                ectx.open_context_menu(*px, *py);
+                true
+            }
+            Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, x: px, y: py, .. } => {
+                if self.disabled { return false; }
+                if !self.editing {
+                    self.begin_editing();
+                    ectx.request_focus();
                 } else {
-                    999999
-                };
-                let (_lines, index_map) = self.wrap_text(max_chars);
-
-                let view_top = self.base.y + top;
-                let view_bottom = self.base.y + self.base.h;
-
-                if start != end {
-                    let start_pos = index_map[start.min(index_map.len() - 1)];
-                    let end_pos = index_map[end.min(index_map.len() - 1)];
-                    
-                    for line_idx in start_pos.0..=end_pos.0 {
-                        let mut line_start_col = None;
-                        let mut line_end_col = None;
-                        for idx in start..end {
-                            if idx < index_map.len() {
-                                let (l, c) = index_map[idx];
-                                if l == line_idx {
-                                    if line_start_col.is_none() || c < line_start_col.unwrap() {
-                                        line_start_col = Some(c);
-                                    }
-                                    if line_end_col.is_none() || c > line_end_col.unwrap() {
-                                        line_end_col = Some(c);
-                                    }
-                                }
-                            }
-                        }
-                        if let (Some(sc), Some(ec)) = (line_start_col, line_end_col) {
-                            let highlight_x = x + 8.0 + (sc as f32 * char_width) - self.scroll_x;
-                            let highlight_w = (ec - sc + 1) as f32 * char_width;
-                            let highlight_y = self.base.y + top + 8.0 + (line_idx as f32 * line_height) - self.scroll_y;
-                            let clipped_y = highlight_y.max(view_top);
-                            let clipped_bottom = (highlight_y + line_height).min(view_bottom);
-                            let h_left = highlight_x.max(x + 8.0);
-                            let h_right = (highlight_x + highlight_w).min(x + w - 8.0);
-                            if h_left < h_right && clipped_y < clipped_bottom {
-                                quads.push((h_left, clipped_y, h_right - h_left, clipped_bottom - clipped_y, 0.0, highlight_color, (false, false, false, false)));
-                            }
-                        }
-                    }
+                    let idx = self.position_to_idx(*px, *py, true);
+                    self.cursor_idx = idx;
+                    self.select_anchor = Some(idx);
+                    self.all_selected = false;
                 }
-                
-                if self.editing {
-                    let caret_h = self.font_size * 1.15;
-                    let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
-                    let cursor_x = x + 8.0 + (cursor_c as f32 * char_width) - self.scroll_x;
-                    let cursor_y = self.base.y + top + 8.0 + (cursor_l as f32 * line_height) + (line_height - caret_h) / 2.0 - self.scroll_y;
-                    let clipped_y = cursor_y.max(view_top);
-                    let clipped_bottom = (cursor_y + caret_h).min(view_bottom);
-                    if cursor_x >= x + 8.0 && cursor_x <= x + w - 8.0 {
-                        if clipped_y < clipped_bottom {
-                            quads.push((cursor_x, clipped_y, 1.5, clipped_bottom - clipped_y, 0.0, cursor_color, (false, false, false, false)));
-                        }
-                    }
+                true
+            }
+            Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, x: px, y: py, .. } => {
+                if self.disabled { return false; }
+                // Legacy gated releases on the hit test; the adapter delivers them ungated, so
+                // re-check containment (plain rect + side inset — row spans approximated).
+                let label_x = side_offset(&self.label);
+                let top = self.label_top();
+                let (bx, by, bw, bh) = (self.rect.x + label_x, self.rect.y, self.rect.width - label_x, self.rect.height);
+                let _ = top;
+                if !(*px >= bx && *px <= bx + bw && *py >= by && *py <= by + bh) {
+                    return false;
                 }
-            } else {
-                let caret_h = self.font_size * 1.15;
-                if start != end {
-                    let h_left_offset = self.glyph_positions.get(start).copied().unwrap_or_else(|| start as f32 * char_width);
-                    let h_right_offset = self.glyph_positions.get(end).copied().unwrap_or_else(|| end as f32 * char_width);
-                    let highlight_x = x + 8.0 + h_left_offset - self.scroll_x;
-                    let h_left = highlight_x.max(x + 8.0);
-                    let h_right = (x + 8.0 + h_right_offset - self.scroll_x).min(x + w - 8.0);
-                    if h_left < h_right {
-                        quads.push((
-                            h_left,
-                            crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, top),
-                            h_right - h_left,
-                            crate::layout::line_height(self.font_size),
-                            0.0,
-                            highlight_color,
-                            (false, false, false, false),
-                        ));
-                    }
+                if self.dragging {
+                    self.dragging = false;
                 }
-
-                if self.editing {
-                    let offset = if self.glyph_positions.is_empty() {
-                        self.cursor_idx as f32 * char_width
-                    } else {
-                        self.cursor_x_offset
-                    };
-                    let cursor_x = x + 8.0 + offset - self.scroll_x;
-                    if cursor_x >= x + 8.0 && cursor_x <= x + w - 8.0 {
-                        let text_y = crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, top);
-                        let cursor_y = text_y + (self.font_size - caret_h) / 2.0;
-                        quads.push((cursor_x, cursor_y, 1.5, caret_h, 0.0, cursor_color, (false, false, false, false)));
-                    }
+                if self.select_anchor == Some(self.cursor_idx) {
+                    self.select_anchor = None;
                 }
+                true
             }
+            Event::PointerMove { x: px, y: py, .. } => {
+                // The drag-selection half of the legacy `on_cursor_moved`; hover bookkeeping
+                // is the adapter's (Enter/Leave below).
+                if self.disabled {
+                    return false;
+                }
+                if self.dragging && self.editing {
+                    return self.extend_selection_to(*px, *py);
+                }
+                false
+            }
+            Event::MouseEnter => {
+                self.hovered = !self.disabled;
+                true
+            }
+            Event::MouseLeave => {
+                self.hovered = false;
+                true
+            }
+            Event::MouseWheel { delta, .. } => self.handle_wheel(delta),
+            Event::KeyInput(key_event) => self.handle_key(key_event),
+            Event::FocusIn => {
+                // The legacy `focus()`: enter editing and claim the global slot (unless
+                // disabled — legacy early-returned before `set_focused`).
+                if !self.disabled {
+                    self.begin_editing();
+                    ectx.request_focus();
+                }
+                false
+            }
+            Event::FocusOut => {
+                self.commit_editing();
+                false
+            }
+            _ => false,
         }
-        
-        for &child_ptr in &self.children(ctx) {
-            let widget = unsafe { &*child_ptr };
-            quads.extend(widget.all_rounded_quads(ctx));
-        }
-        quads
     }
 
-    fn text_labels(&self) -> Vec<TextLabel> {
-        let mut labels = Vec::new();
-        let top = self.base.label_offset();
-        let _visual_h = self.base.h - top;
-        if let Some(lbl) = self.control_label() {
-            labels.push(lbl);
-        }
-        let mut val_text = if self.editing {
-            self.edit_buffer.clone()
-        } else {
-            self.text.clone()
-        };
-        if self.is_password {
-            val_text = "•".repeat(val_text.chars().count());
-        }
+    fn take_change(&mut self) -> bool {
+        self.take_change()
+    }
 
-        let is_placeholder = val_text.is_empty() && self.placeholder.is_some();
-        let display_text = if is_placeholder {
-            self.placeholder.as_ref().unwrap().clone()
-        } else {
-            val_text
-        };
+    fn value_string(&self) -> Option<String> {
+        Some(self.text.clone())
+    }
 
-        let label_color = if is_placeholder {
-            crate::colors::textbox_placeholder_text_color()
-        } else if let Some(custom_color) = self.text_color {
-            custom_color
-        } else 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]
-        };
+    fn set_value_string(&mut self, val: &str) -> bool {
+        self.set_value(val)
+    }
 
-        let label_x = self.label_x_offset();
-        let x = self.base.x + label_x;
-        let w = self.base.w - label_x;
+    fn cut_selection(&mut self) -> bool {
+        let res = self.cut_selection();
+        if res {
+            self.just_changed = true;
+        }
+        res
+    }
 
-        if self.multiline {
-            let char_width = self.char_width();
-            let line_height = self.line_height();
-            let max_chars = if self.line_wrap_enabled() {
-                (((w - 16.0) / char_width).floor() as usize).max(1)
-            } else {
-                999999
-            };
-            let (lines, _) = self.wrap_text(max_chars);
-            let lines_to_draw = if is_placeholder {
-                let placeholder_src = self.placeholder.as_ref().unwrap();
-                let chars: Vec<char> = placeholder_src.chars().collect();
-                let mut p_lines = Vec::new();
-                let mut current_line = Vec::new();
-                for ch in chars {
-                    if ch == '\n' {
-                        p_lines.push(current_line.iter().collect::<String>());
-                        current_line.clear();
-                    } else {
-                        current_line.push(ch);
-                        if self.line_wrap_enabled() && current_line.len() > max_chars {
-                            p_lines.push(current_line.iter().collect::<String>());
-                            current_line.clear();
-                        }
-                    }
-                }
-                p_lines.push(current_line.iter().collect::<String>());
-                p_lines
-            } else {
-                lines
-            };
-            for (line_idx, line_text) in lines_to_draw.iter().enumerate() {
-                labels.push(TextLabel {
-                    text: line_text.clone(),
-                    x: x + 8.0 - self.scroll_x,
-                    y: self.base.y + top + 8.0 + (line_idx as f32 * line_height) + (line_height - self.font_size) / 2.0 - self.scroll_y,
-                    font_size: self.font_size,
-                    color: label_color,
-                });
-            }
-        } else {
-            labels.push(TextLabel {
-                text: display_text,
-                x: x + 8.0 - self.scroll_x,
-                y: crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, top),
-                font_size: self.font_size,
-                color: label_color,
-            });
+    fn copy_selection(&self) {
+        self.copy_selection();
+    }
+
+    fn paste_from_clipboard(&mut self) -> bool {
+        let res = self.paste_from_clipboard();
+        if res {
+            self.just_changed = true;
         }
-        labels
+        res
     }
 
-    fn text_labels_with_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
-        let label_x = self.label_x_offset();
-        let bounds = Some([self.base.x + label_x, self.base.y, self.base.x + self.base.w, self.base.y + self.base.h]);
-        self.text_labels().into_iter().map(|l| (l, bounds)).collect()
+    fn select_all(&mut self) {
+        self.select_all();
     }
 
-    fn text_labels_with_font_and_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        let font = self.widget_font();
-        let label_x = self.label_x_offset();
-        let bounds = Some([self.base.x + label_x, self.base.y, self.base.x + self.base.w, self.base.y + self.base.h]);
-        self.text_labels().into_iter().map(|l| (l, font.clone(), bounds)).collect()
+    fn clear_text(&mut self) {
+        self.set_value("");
     }
 
-    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, _px: f32, _py: f32, ctx: &mut UiContext) -> bool {
-        if self.disabled { return false; }
-        let char_width = self.char_width();
-        let line_height = self.line_height();
-        
-        let max_chars = if self.line_wrap_enabled() {
-            (((self.base.w - 16.0) / char_width).floor() as usize).max(1)
-        } else {
-            999999
-        };
-        
-        let (lines, _) = if self.multiline {
-            self.wrap_text(max_chars)
-        } else {
-            let buffer = if self.editing { &self.edit_buffer } else { &self.text };
-            (vec![buffer.clone()], vec![(0, 0); buffer.chars().count() + 1])
-        };
+    fn draggable(&self, _rect: Rect) -> bool {
+        !self.disabled
+    }
 
-        let mut changed = false;
+    fn is_dragging(&self) -> bool {
+        self.dragging
+    }
 
-        if self.multiline {
-            let content_h = lines.len() as f32 * line_height;
-            let max_scroll = (content_h - (self.base.h - 16.0)).max(0.0);
-            let scroll_amt = match *delta {
-                MouseScrollDelta::LineDelta(_, dy) => -dy * line_height * 2.0,
-                MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
-            };
-            let old_scroll = self.scroll_y;
-            self.scroll_y = (self.scroll_y + scroll_amt).clamp(0.0, max_scroll);
-            if old_scroll != self.scroll_y {
-                changed = true;
-            }
-        }
+    fn drag_begin(&mut self, _px: f32, _py: f32, _rect: Rect) {
+        if self.disabled || !self.editing { return; }
+        self.dragging = true;
+    }
 
-        if !self.line_wrap_enabled() {
-            let max_line_len = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
-            let content_w = max_line_len as f32 * char_width;
-            let max_scroll_x = (content_w - (self.base.w - 16.0)).max(0.0);
-            let natural = crate::layout::touchpad_natural_scroll();
-            let scroll_amt_x = match *delta {
-                MouseScrollDelta::LineDelta(dx, dy) => {
-                    if !self.multiline {
-                        let scroll_val = if dy != 0.0 { -dy } else { if natural { -dx } else { dx } };
-                        scroll_val * char_width * 3.0
-                    } else {
-                        let scroll_val = if natural { -dx } else { dx };
-                        scroll_val * char_width * 3.0
-                    }
-                }
-                MouseScrollDelta::PixelDelta(pos) => {
-                    if !self.multiline {
-                        let scroll_val = if pos.y != 0.0 { -pos.y as f32 } else { if natural { -pos.x as f32 } else { pos.x as f32 } };
-                        scroll_val
-                    } else {
-                        if natural { -pos.x as f32 } else { pos.x as f32 }
-                    }
-                }
-            };
-            let old_scroll_x = self.scroll_x;
-            self.scroll_x = (self.scroll_x + scroll_amt_x).clamp(0.0, max_scroll_x);
-            if old_scroll_x != self.scroll_x {
-                changed = true;
-            }
-        }
+    fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
+        if self.disabled || !self.editing { return false; }
+        self.extend_selection_to(px, py)
+    }
 
-        if changed {
-            self.mark_dirty(ctx);
-            true
-        } else {
-            false
-        }
+    fn drag_end(&mut self) {
+        self.dragging = false;
     }
 }
 
-impl Drop for TextBox {
-    fn drop(&mut self) {
-        clear_widget_references(self);
+/// Legacy `Default` (an empty box) — settings' accounts page derives `Default` over fields of
+/// this type.
+impl Default for Adapted<TextBox> {
+    fn default() -> Self {
+        TextBox::new(String::new())
     }
 }
 
 unsafe impl Send for TextBox {}
 unsafe impl Sync for TextBox {}
 
-impl Control for TextBox {}
+impl Control for Adapted<TextBox> {
+    fn set_label(&mut self, label: &str) {
+        Adapted::set_label(self, label);
+    }
+}
 
 #[cfg(test)]
 mod tests {
@@ -1664,12 +1627,14 @@ mod tests {
         tb.set_rect(10.0, 10.0, 200.0, 100.0);
         tb.select_anchor = Some(7); // starts at "Line 2"
         tb.cursor_idx = 13;        // ends at end of "Line 2"
-        
+
         let has_rounded = tb.rounded_corners() != (false, false, false, false);
         let has_highlight = if has_rounded {
             let rounded = tb.all_rounded_quads(&dummy);
             println!("Rounded quads: {:?}", rounded);
+            let quads = tb.all_quads(&dummy);
             rounded.iter().any(|q| q.5 == [0.20, 0.50, 0.85, 0.3])
+                || quads.iter().any(|q| q.4 == [0.20, 0.50, 0.85, 0.3])
         } else {
             let extra = tb.extra_quads();
             println!("Extra quads: {:?}", extra);
@@ -1682,18 +1647,18 @@ mod tests {
     fn test_textbox_line_wrap_disabled_horizontal_scrolling() {
         let _dummy = crate::context::UiContext::new();
         crate::layout::set_textbox_line_wrap(false);
-        
+
         let mut tb = TextBox::new("Very long text that should not wrap and instead scroll horizontally".to_string());
         tb.set_rect(10.0, 10.0, 100.0, 30.0);
-        
+
         assert_eq!(tb.scroll_x, 0.0);
-        
+
         tb.focus();
         tb.cursor_idx = tb.edit_buffer.chars().count();
         tb.scroll_to_cursor();
-        
+
         assert!(tb.scroll_x > 0.0, "scroll_x should be scrolled horizontally to keep the cursor visible");
-        
+
         crate::layout::set_textbox_line_wrap(true);
     }
 
@@ -1713,7 +1678,7 @@ mod tests {
         assert!(opts.contains(&"Cear".to_string()));
 
         // Simulate choosing the "Cear" option
-        tb.clear_text();
+        Element::clear_text(&mut tb);
         assert_eq!(tb.text, "");
         assert_eq!(tb.edit_buffer, "");
     }
@@ -1738,4 +1703,3 @@ mod tests {
         crate::layout::set_textbox_multiline_border_width(1.0);
     }
 }
-
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 3fd809a..8f2d0d4 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -93,6 +93,27 @@ pub trait Layout {
         false
     }
 
+    /// Whether the adapter's hit test substitutes the base row rect (`row_x`/`row_w`, pushed in
+    /// by row-layout hosts via `set_row_rect`) plus the side-label inset — the legacy
+    /// `Element::hit_test` default geometry. Migrated controls so far dropped it (accepted
+    /// drift); TextBox restores it (cce-files' save-name box relies on row hits). Default: off,
+    /// keeping the other migrated widgets exactly as they shipped.
+    fn hit_row_rect(&self) -> bool {
+        false
+    }
+
+    /// Adjust a row-rect assignment before it lands on the base (`Element::set_row_rect` —
+    /// TextBox clamps the row width to its `width`/`max_width`). Default: identity.
+    fn adjust_row_rect(&self, x: f32, w: f32) -> (f32, f32) {
+        (x, w)
+    }
+
+    /// The final base rect landed from a `set_rect`, visible or not — unlike
+    /// [`arrange_children`](Layout::arrange_children), which the adapter gates on visibility.
+    /// TextBox caches it (its cursor/scroll math reads the laid-out rect between events) and
+    /// re-clamps its scroll, the legacy `set_rect` side effect. Default: ignore.
+    fn rect_assigned(&mut self, _rect: Rect) {}
+
     // --- Container concern (transitional). Legacy containers own `Vec<*mut dyn Element>`
     // children (child-arranging `set_rect` has no ctx to reach the tree) and every one
     // hand-copies the same subtree plumbing: geometry/text aggregation, tick/popover/text-item
@@ -251,6 +272,21 @@ pub trait Paint {
     fn text_bounds(&self, _rect: Rect) -> Option<[f32; 4]> {
         None
     }
+
+    /// Per-frame text shaping against the app's `FontSystem` (legacy `Element::prepare_text`
+    /// overrides). TextBox measures its glyph advances here — load-bearing for cursor↔pixel
+    /// mapping, not just a render cache. Receives the laid-out content rect. Default: nothing
+    /// to shape.
+    fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem, _rect: Rect) {}
+
+    /// Whether the adapter re-enables the legacy shared focus/hover highlight overlay
+    /// (`Element::highlight_quad`'s default) for this widget. The adapter suppresses it for
+    /// migrated widgets — matching the `None` overrides most legacy controls carried — but
+    /// legacy TextBox kept the default: the focused editor gets the primary-highlight tint
+    /// over its background (data-editor's teal editing wash). Default: suppressed.
+    fn legacy_focus_highlight(&self) -> bool {
+        false
+    }
 }
 
 /// What an event handler may reach beyond its own state — the RFC §3.5 `EventCtx`, grown as
@@ -379,6 +415,43 @@ pub trait Input {
         0
     }
 
+    // --- Clipboard/selection surface (the context menu's Cut/Copy/Paste/Select-All actions
+    // call these on their target Element). The defaults replicate the `Element` defaults
+    // byte-for-byte (whole-value copy through the value-string pair), so widgets migrated
+    // before these hooks existed keep their exact behavior; TextBox overrides with real
+    // selection-aware implementations.
+
+    fn cut_selection(&mut self) -> bool {
+        if let Some(val) = self.value_string() {
+            crate::widget::clipboard::copy_to_clipboard(&val);
+            self.set_value_string("")
+        } else {
+            false
+        }
+    }
+    fn copy_selection(&self) {
+        if let Some(val) = self.value_string() {
+            crate::widget::clipboard::copy_to_clipboard(&val);
+        }
+    }
+    fn paste_from_clipboard(&mut self) -> bool {
+        if let Some(text) = crate::widget::clipboard::read_from_clipboard() {
+            self.set_value_string(&text)
+        } else {
+            false
+        }
+    }
+    fn select_all(&mut self) {}
+    fn clear_text(&mut self) {}
+
+    /// Whether direct `focus()`/`unfocus()` calls flip the base `focused` flag. Legacy widgets
+    /// differ: most set it in their `focus` overrides, but TextBox never did — its detached
+    /// label must not color as focused. Default: flip it (what every widget migrated so far
+    /// has shipped with).
+    fn tracks_base_focus(&self) -> bool {
+        true
+    }
+
     /// Selection state pushed in by list/row hosts (legacy `Element::set_selected`).
     fn set_selected(&mut self, _selected: bool) {}
 
@@ -873,6 +946,8 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
 
     fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
         if self.visible() {
+            let rect = self.content_rect();
+            Paint::prepare_text(&mut self.inner, fs, rect);
             for child in self.visible_children() {
                 unsafe { (*child).prepare_text(fs) };
             }
@@ -928,6 +1003,10 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         self.base.y = r.y;
         self.base.w = r.width;
         self.base.h = r.height + inflation;
+        // Ungated rect notification (TextBox re-clamps scroll on every assignment, hidden or
+        // not — the legacy `set_rect` side effect).
+        let landed = Rect { x: self.base.x, y: self.base.y, width: self.base.w, height: self.base.h };
+        Layout::rect_assigned(&mut self.inner, landed);
         // Containers position their children from the assigned rect (legacy `set_rect`
         // overrides); hidden containers skip it, like the legacy impls.
         if self.visible {
@@ -959,9 +1038,26 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
 
     /// Narrow widgets own every pixel they draw through [`Paint::paint`]; the legacy shared
     /// hover-highlight overlay is suppressed (matching what most control widgets' `None`
-    /// overrides do today).
-    fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
-        None
+    /// overrides do today) — unless the widget opts back in
+    /// ([`Paint::legacy_focus_highlight`], TextBox), in which case this replicates the
+    /// `Element` default byte-for-byte: primary tint when ctx-focused (or active), secondary
+    /// when hovered, over the row-substituted, side-label-inset span.
+    fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+        if !Paint::legacy_focus_highlight(&self.inner) {
+            return None;
+        }
+        let is_focused = ctx.is_focused_addr(self as *const Self as *const () as usize);
+        let hc = if is_focused {
+            crate::colors::highlight_primary_color()
+        } else if self.base.hovered {
+            crate::colors::HIGHLIGHT_SECONDARY
+        } else {
+            return None;
+        };
+        let label_x = Element::label_x_offset(self);
+        let hx = if self.base.row_w > 0.0 { self.base.row_x } else { self.base.x } + label_x;
+        let hw = if self.base.row_w > 0.0 { self.base.row_w } else { self.base.w } - label_x;
+        Some((hx, self.base.y, hw, self.base.h, hc))
     }
 
     /// Report the *inner* type's name, not `Adapted<W>`: runtime type-name matching (e.g.
@@ -997,8 +1093,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn widget_font(&self) -> Option<String> {
         Paint::widget_font(&self.inner)
     }
-    fn paint_self(&self, _ui: &UiContext, ctx: &mut PaintCtx) {
+    fn paint_self(&self, ui: &UiContext, ctx: &mut PaintCtx) {
         Paint::paint(&self.inner, self.content_rect(), ctx);
+        // The legacy default `paint_self` drained `all_quads`, which carries the focus
+        // highlight — replicate for opt-in widgets, over the background (same draw order).
+        if let Some((hx, hy, hw, hh, hc)) = Element::highlight_quad(self, ui) {
+            if hc != crate::colors::HIGHLIGHT_SECONDARY {
+                ctx.quad(Rect { x: hx, y: hy, width: hw, height: hh }, hc);
+            }
+        }
         // Inline-label widgets emit their own text in `paint`; detached labels come from the
         // base, exactly as the legacy default `paint_self` emits them.
         if !Layout::inline_label(&self.inner) {
@@ -1103,6 +1206,13 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
             return Vec::new();
         }
         let mut quads = self.extra_quads();
+        // The `Element` default's highlight inclusion (secondary/hover tint excluded), live
+        // only for widgets that opt into the legacy overlay.
+        if let Some(hq) = Element::highlight_quad(self, ctx) {
+            if hq.4 != crate::colors::HIGHLIGHT_SECONDARY {
+                quads.push(hq);
+            }
+        }
         if self.visible() {
             // Container aggregation, replicating the shared legacy loop (Layer, Switcher):
             // children contribute their plain quads, except a rounded-cornered child's
@@ -1177,6 +1287,29 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn set_selected(&mut self, selected: bool) {
         Input::set_selected(&mut self.inner, selected)
     }
+    fn cut_selection(&mut self) -> bool {
+        Input::cut_selection(&mut self.inner)
+    }
+    fn copy_selection(&self) {
+        Input::copy_selection(&self.inner)
+    }
+    fn paste_from_clipboard(&mut self) -> bool {
+        Input::paste_from_clipboard(&mut self.inner)
+    }
+    fn select_all(&mut self) {
+        Input::select_all(&mut self.inner)
+    }
+    fn clear_text(&mut self) {
+        Input::clear_text(&mut self.inner)
+    }
+    /// Row-rect assignment (row-layout hosts): apply the widget's clamp
+    /// ([`Layout::adjust_row_rect`] — TextBox's `width`/`max_width`), then the base write the
+    /// `Element` default does.
+    fn set_row_rect(&mut self, x: f32, w: f32) {
+        let (rx, rw) = Layout::adjust_row_rect(&self.inner, x, w);
+        self.base.row_x = rx;
+        self.base.row_w = rw;
+    }
     fn draggable(&self) -> bool {
         Input::draggable(&self.inner, self.content_rect())
     }
@@ -1318,16 +1451,22 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         }
     }
 
-    /// Focus set/cleared directly (hosts call `w.focus()`/`w.unfocus()`): keep the base flag and
-    /// tell the widget via the same `FocusIn`/`FocusOut` events the router would send.
+    /// Focus set/cleared directly (hosts call `w.focus()`/`w.unfocus()`): keep the base flag
+    /// (unless the widget opts out — [`Input::tracks_base_focus`], TextBox's legacy `focus`
+    /// never set it) and tell the widget via the same `FocusIn`/`FocusOut` events the router
+    /// would send.
     fn focus(&mut self) {
-        self.base.focused = true;
+        if Input::tracks_base_focus(&self.inner) {
+            self.base.focused = true;
+        }
         let self_ptr = self.as_ptr_mut();
         let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
         Input::on_event(&mut self.inner, &Event::FocusIn, &mut ectx);
     }
     fn unfocus(&mut self) {
-        self.base.focused = false;
+        if Input::tracks_base_focus(&self.inner) {
+            self.base.focused = false;
+        }
         let self_ptr = self.as_ptr_mut();
         let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
         Input::on_event(&mut self.inner, &Event::FocusOut, &mut ectx);
@@ -1355,6 +1494,19 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
             return false;
         }
         let (x, y, w, h) = self.rect();
+        // Row-hit opt-in ([`Layout::hit_row_rect`]): replicate the legacy `hit_test` default's
+        // geometry — substitute the host-pushed row span and inset by the side label — before
+        // the narrow test. The width<=0 reject also comes from that default.
+        if Layout::hit_row_rect(&self.inner) {
+            if w <= 0.0 || h <= 0.0 {
+                return false;
+            }
+            let (mut hx, mut hw) = if self.base.row_w > 0.0 { (self.base.row_x, self.base.row_w) } else { (x, w) };
+            let label_x = Element::label_x_offset(self);
+            hx += label_x;
+            hw -= label_x;
+            return Input::hit(&self.inner, Rect { x: hx, y, width: hw, height: h }, px, py);
+        }
         Input::hit(&self.inner, Rect { x, y, width: w, height: h }, px, py)
     }