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

commit136910a2f0bd2ac8d2eec6e7f60ab0bd2133f2c7
parent57f2a9da97
authorLucas Galante <[email protected]>
date2026-07-01 10:10
Fix selection highlight layout order overlap with rounded corners

 src/widget/input/text_box.rs | 163 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 161 insertions(+), 2 deletions(-)

diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 4b45333..3013695 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -782,6 +782,12 @@ impl Element for TextBox {
 
     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;
         if self.disabled {
@@ -880,8 +886,7 @@ impl Element for TextBox {
                         quads.push((cursor_x, clipped_y, 1.5, clipped_bottom - clipped_y, cursor_color));
                     }
                 }
-            }
- else {
+            } else {
                 let caret_h = self.font_size * 1.15;
                 if start != end {
                     let highlight_x = self.base.x + 8.0 + (start as f32 * char_width);
@@ -909,6 +914,139 @@ impl Element for TextBox {
         quads
     }
 
+    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));
+            }
+            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 {
+            [0.06, 0.10, 0.18, 1.0]
+        } else {
+            [0.08, 0.08, 0.12, 1.0]
+        };
+        let border_color = if self.editing {
+            [0.20, 0.50, 0.85, 1.0]
+        } else if self.base.hovered {
+            [0.25, 0.25, 0.35, 1.0]
+        } else {
+            [0.18, 0.18, 0.24, 1.0]
+        };
+        
+        if self.draw_bg_border {
+            quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, radius, border_color, (r1, r2, r3, r4)));
+            quads.push((self.base.x + 1.0, self.base.y + top + 1.0, self.base.w - 2.0, visual_h - 2.0, radius - 1.0, bg_color, (r1, r2, r3, r4)));
+        }
+
+        if self.editing || self.select_anchor.is_some() {
+            let char_width = self.font_size * 0.6;
+            let line_height = self.font_size * 1.333;
+            
+            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 = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
+                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 = self.base.x + 8.0 + (sc as f32 * char_width);
+                            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);
+                            if clipped_y < clipped_bottom {
+                                quads.push((highlight_x, clipped_y, highlight_w, clipped_bottom - clipped_y, 0.0, highlight_color, (false, false, false, 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 = self.base.x + 8.0 + (cursor_c as f32 * char_width);
+                    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 clipped_y < clipped_bottom {
+                        quads.push((cursor_x, clipped_y, 1.5, clipped_bottom - clipped_y, 0.0, cursor_color, (false, false, false, false)));
+                    }
+                }
+            } else {
+                let caret_h = self.font_size * 1.15;
+                if start != end {
+                    let highlight_x = self.base.x + 8.0 + (start as f32 * char_width);
+                    let max_x = self.base.x + self.base.w - 6.0;
+                    let highlight_w = ((end - start) as f32 * char_width).min(max_x - highlight_x).max(0.0);
+                    quads.push((
+                        highlight_x,
+                        crate::layout::align_text_y(self.base.y, self.base.h, self.font_size, top),
+                        highlight_w,
+                        crate::layout::line_height(self.font_size),
+                        0.0,
+                        highlight_color,
+                        (false, false, false, false),
+                    ));
+                }
+
+                if self.editing {
+                    let cursor_x = self.base.x + 8.0 + (self.cursor_idx as f32 * char_width);
+                    let max_cursor_x = self.base.x + self.base.w - 6.0;
+                    let final_cursor_x = cursor_x.min(max_cursor_x);
+                    let cursor_y = self.base.y + top + (visual_h - caret_h) / 2.0;
+                    quads.push((final_cursor_x, cursor_y, 1.5, caret_h, 0.0, cursor_color, (false, false, 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();
@@ -1196,6 +1334,27 @@ mod tests {
         assert!(clicked);
         assert!(dummy.is_context_menu_visible());
         assert!(tb.editing);
+     }
+
+    #[test]
+    fn test_textbox_multiline_selection_highlight() {
+        let dummy = crate::context::UiContext::new();
+        let mut tb = TextBox::new("Line 1\nLine 2\nLine 3".to_string()).with_multiline(true);
+        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);
+            rounded.iter().any(|q| q.5 == [0.20, 0.50, 0.85, 0.3])
+        } else {
+            let extra = tb.extra_quads();
+            println!("Extra quads: {:?}", extra);
+            extra.iter().any(|q| q.4 == [0.20, 0.50, 0.85, 0.3])
+        };
+        assert!(has_highlight, "Should have a highlight quad!");
     }
 }