git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commit9b091fea34b699d23ee1a2859d013aa0b1718231
parentac5927391e
authorLucas Galante <[email protected]>
date2026-08-22 16:31
feat: clicking into the URL bar selects the whole URL

Entering an unfocused bar put the caret where the click landed, so
replacing the URL meant Ctrl+U or holding backspace first. It now selects
the entire contents, the way every other browser's address bar does, and
the next keystroke replaces them.

This gives the hand-rolled editor its first notion of a selection — a
normalized byte range that any edit either replaces (typing, backspace,
delete, all through take_selection) or drops (arrows collapse to the edge
they move toward; Home/End/Ctrl+U/Escape clear). Every path that rewrites
url_input from elsewhere — navigate, new tab, the page-driven sync, losing
focus — clears it too, so a range can never outlive the text it indexes.
Clicking an already-focused bar still places the caret, unchanged.

The highlight is a quad under the text inside the field clip, positioned
from shaped_cluster_offsets — the same shaped buffer that draws the glyphs
and places the caret (b280e6b), not measured extents.

Shadow-verified: click selects the whole URL with the caret at the end;
typing replaces it; backspace clears it in one press; a second click drops
the selection and places the caret, and typing there inserts mid-text.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/main.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 86 insertions(+), 13 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index c06a591..f61181c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -56,6 +56,8 @@ const TAB_BG: [f32; 4] = [0.15, 0.16, 0.18, 0.30];
 const TAB_ACTIVE_BG: [f32; 4] = [0.32, 0.34, 0.38, 0.55];
 const RIM: [f32; 4] = [0.22, 0.23, 0.25, 1.0];
 const RIM_FOCUS: [f32; 4] = [0.33, 0.48, 0.72, 1.0];
+/// URL-bar selection highlight; the text is drawn over it.
+const SEL_BG: [f32; 4] = [0.24, 0.38, 0.60, 0.95];
 const ACCENT: [f32; 4] = [0.35, 0.55, 0.85, 1.0];
 const TEXT: [u8; 3] = [220, 220, 225];
 const TEXT_DIM: [u8; 3] = [120, 122, 128];
@@ -80,6 +82,9 @@ struct BrowserApp {
     url_focused: bool,
     /// Byte index of the URL-bar cursor.
     cursor: usize,
+    /// Selected byte range in the URL bar, normalized (start < end). Set by
+    /// clicking into an unfocused bar; any edit replaces or drops it.
+    selection: Option<(usize, usize)>,
     loading: bool,
     /// Page title; drives the toplevel title (the engine re-applies
     /// `settings().title` whenever it changes).
@@ -316,6 +321,7 @@ impl BrowserApp {
                 let s = u.to_string();
                 self.url_input = if s == "about:blank" { String::new() } else { s };
                 self.cursor = self.url_input.len();
+                self.selection = None;
             }
         }
     }
@@ -324,6 +330,7 @@ impl BrowserApp {
         if let Some(url) = parse_url_input(&self.url_input, &self.settings.search_prefix) {
             self.host.load(url);
             self.url_focused = false;
+            self.selection = None;
             self.loading = true;
         }
     }
@@ -347,6 +354,7 @@ impl BrowserApp {
         self.host.open_tab(url);
         self.url_input.clear();
         self.cursor = 0;
+        self.selection = None;
         self.url_focused = true;
         self.sync_page_state();
     }
@@ -416,47 +424,85 @@ impl BrowserApp {
             .unwrap_or(self.url_input.len())
     }
 
-    /// Caret x offset (text-origin relative) for the current byte cursor, off
-    /// the same shaped buffer as `cursor_from_click`.
-    fn caret_offset(&mut self) -> f32 {
+    /// X offset (text-origin relative) of a byte index, off the same shaped
+    /// buffer as `cursor_from_click`.
+    fn x_offset(&mut self, byte: usize) -> f32 {
         let offsets = cce_ui::engine::shaped_cluster_offsets(&mut self.font_system, &self.url_input, URL_FONT, None);
-        let cursor = self.cursor;
         offsets
             .iter()
             .rev()
-            .find(|&&(b, _)| b <= cursor)
+            .find(|&&(b, _)| b <= byte)
             .map(|&(_, x)| x)
             .unwrap_or(0.0)
     }
 
+    /// Caret x offset for the current byte cursor.
+    fn caret_offset(&mut self) -> f32 {
+        self.x_offset(self.cursor)
+    }
+
+    /// Drop a selection, deleting its text first if it covers any. Returns
+    /// whether text was removed, so edits can treat "replace the selection"
+    /// and "act at the cursor" as one path.
+    fn take_selection(&mut self) -> bool {
+        match self.selection.take() {
+            Some((a, b)) if a < b && b <= self.url_input.len() => {
+                self.url_input.replace_range(a..b, "");
+                self.cursor = a;
+                true
+            }
+            _ => false,
+        }
+    }
+
     fn edit_url(&mut self, event: &KeyEvent) {
         match &event.logical_key {
             Key::Named(NamedKey::Enter) => self.navigate(),
             Key::Named(NamedKey::Escape) => {
                 self.url_focused = false;
+                self.selection = None;
                 self.sync_page_state();
             }
             Key::Named(NamedKey::Backspace) => {
-                if self.cursor > 0 {
+                if !self.take_selection() && self.cursor > 0 {
                     let prev = prev_boundary(&self.url_input, self.cursor);
                     self.url_input.replace_range(prev..self.cursor, "");
                     self.cursor = prev;
                 }
             }
             Key::Named(NamedKey::Delete) => {
-                if self.cursor < self.url_input.len() {
+                if !self.take_selection() && self.cursor < self.url_input.len() {
                     let next = next_boundary(&self.url_input, self.cursor);
                     self.url_input.replace_range(self.cursor..next, "");
                 }
             }
-            Key::Named(NamedKey::ArrowLeft) => self.cursor = prev_boundary(&self.url_input, self.cursor),
-            Key::Named(NamedKey::ArrowRight) => self.cursor = next_boundary(&self.url_input, self.cursor),
-            Key::Named(NamedKey::Home) => self.cursor = 0,
-            Key::Named(NamedKey::End) => self.cursor = self.url_input.len(),
+            // Arrows collapse a selection to the edge they move toward,
+            // rather than stepping from the cursor.
+            Key::Named(NamedKey::ArrowLeft) => {
+                self.cursor = match self.selection.take() {
+                    Some((a, _)) => a,
+                    None => prev_boundary(&self.url_input, self.cursor),
+                };
+            }
+            Key::Named(NamedKey::ArrowRight) => {
+                self.cursor = match self.selection.take() {
+                    Some((_, b)) => b,
+                    None => next_boundary(&self.url_input, self.cursor),
+                };
+            }
+            Key::Named(NamedKey::Home) => {
+                self.selection = None;
+                self.cursor = 0;
+            }
+            Key::Named(NamedKey::End) => {
+                self.selection = None;
+                self.cursor = self.url_input.len();
+            }
             Key::Character(c) if event.ctrl => {
                 if c == "u" {
                     self.url_input.clear();
                     self.cursor = 0;
+                    self.selection = None;
                 }
             }
             _ => {
@@ -467,6 +513,7 @@ impl BrowserApp {
                     _ => None,
                 };
                 if let Some(t) = insert {
+                    self.take_selection();
                     self.url_input.insert_str(self.cursor, &t);
                     self.cursor += t.len();
                 }
@@ -501,6 +548,7 @@ impl Application for BrowserApp {
             url_input,
             url_focused: false,
             cursor,
+            selection: None,
             loading: true,
             title: None,
             font_system: cce_ui::create_font_system(),
@@ -607,10 +655,19 @@ impl Application for BrowserApp {
             } else {
                 let field = url_rect(&bar);
                 if hit(&field, pos.x, pos.y) {
-                    self.cursor = self.cursor_from_click(pos.x, &field);
-                    self.url_focused = true;
+                    if self.url_focused {
+                        self.cursor = self.cursor_from_click(pos.x, &field);
+                        self.selection = None;
+                    } else {
+                        // Entering the bar selects the whole URL, so typing
+                        // replaces it instead of appending to it.
+                        self.url_focused = true;
+                        self.cursor = self.url_input.len();
+                        self.selection = (!self.url_input.is_empty()).then_some((0, self.cursor));
+                    }
                 } else {
                     self.url_focused = false;
+                    self.selection = None;
                 }
             }
             return None;
@@ -619,6 +676,7 @@ impl Application for BrowserApp {
         // Page area: a click dismisses URL-bar focus, then goes to the page.
         if self.url_focused && pressed {
             self.url_focused = false;
+            self.selection = None;
             self.sync_page_state();
             *needs_rebuild = true;
         }
@@ -862,7 +920,22 @@ impl Application for BrowserApp {
         pc.rounded_rect(f, 6.0, (true, true, true, true), FIELD_BG);
         let ty = cce_ui::layout::align_text_y(f.y, f.height, URL_FONT, 0.0);
         let caret_x = if self.url_focused { Some(self.caret_offset()) } else { None };
+        let sel_x = self
+            .selection
+            .filter(|&(a, b)| a < b)
+            .map(|(a, b)| (self.x_offset(a), self.x_offset(b)));
         pc.clip(f, |pc| {
+            if let Some((x0, x1)) = sel_x {
+                pc.quad(
+                    Rect {
+                        x: f.x + URL_PAD_X + x0,
+                        y: f.y + 4.0,
+                        width: x1 - x0,
+                        height: f.height - 8.0,
+                    },
+                    SEL_BG,
+                );
+            }
             pc.text(self.url_input.clone(), f.x + URL_PAD_X, ty, URL_FONT, TEXT);
             if let Some(offset) = caret_x {
                 pc.quad(