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

commit1bf3b47b84fa8d841004f1080b460081be84a993
parent9a97aa0ec7
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 21:58
fix: draw every chrome symbol as a cce-icons glyph

The bar's `<` `>` `R` `*` `B`, the tab close and bookmark-row remove `x`,
the new-tab `+` and the select list's current-option dot were characters
or a primitive standing in for symbols. They are cce-icons glyphs now,
through one `BrowserApp::glyph` (PaintCtx::icon, tinted like text, a
plain fallback word when the icon set is missing): arrow-left /
arrow-right / refresh, x, plus, star (accent-lit when saved), bookmarks
and check. The favorites page's move up/down entities are cce-icons'
arrow-up / arrow-down inlined as SVG in currentColor, copied rather than
include_str!'d so a standalone clone still builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md    |  47 ++++++++++++++++++++++---
 src/main.rs  | 113 +++++++++++++++++++++++++++++------------------------------
 src/pages.rs |  28 ++++++++++++---
 3 files changed, 121 insertions(+), 67 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index b93707a..aed01ac 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -156,6 +156,36 @@ explained there. **Do not reintroduce a `Vec` allocation, a swizzle, or a
 second copy on this path without measuring** — the numbers above are what each
 one costs.
 
+### Only the damage is read
+
+Every webview runs with WebKit's `PropagateDamagingInformation` feature on
+(`FEATURES` in `host.rs`), so each frame says what it repainted. The sink keeps
+that per view — *including frames handed back unread*, whose changes the next
+read still has to carry — and `read_frame` copies only those rectangles
+(`src/wpe/damage.rs`) and hands them to `cce_ui::vk::update_pixel_regions`,
+which writes them into the tab's existing image in one submission. A whole
+frame is read when the tab has no image at that size yet, when a frame said
+nothing (no rectangles = unknown), or when the damage covers half the frame or
+more. Measured in a shadow: an overlay scrollbar fading costs 0.27 MB a frame
+instead of 15 MB, and on Muji with its sliding banner the browser's own CPU
+went from 15% to 10% of a core.
+
+A frame belongs to **the tab that drew it** (`index` in `pump`), not to the
+active tab; a view no tab owns (the spare) is released unread. Before this the
+newest frame from any view was uploaded as the active tab's picture.
+
+Two switches: `CCE_BROWSER_FULL_FRAMES=1` reads whole frames as before (the
+escape hatch if a page is ever drawn stale), and `CCE_BROWSER_DAMAGE_CHECK=1`
+keeps a CPU copy of each tab patched region by region and compares it with
+the whole frame every time, logging any pixel that disagrees. It was exact on
+every read through loading, idling, scrolling, a resize and Muji. Run it after
+touching any of this.
+
+WebKit also gets `HiddenPageCSSAnimationSuspension`: a background tab's CSS
+animations stop, as its `requestAnimationFrame` already does. Its timers still
+run — WebKit has no hidden-page timer throttling here — which is most of what a
+heavy page costs in the background.
+
 ### The readback is paced to draws
 
 `render_buffer` says the two halves of the buffer protocol at different times,
@@ -290,6 +320,13 @@ every hit test in `handle_mouse_input` re-derives the same rects from the same
 `bar_rect`/`tab_rect`/`btn_rect`/`url_rect`/`fav_rects` helpers. **Draw and hit-test are two
 readings of one geometry** — change a rect helper, not one call site.
 
+**Every symbol the chrome draws is a cce-icons glyph** through `BrowserApp::glyph`
+(`PaintCtx::icon`, tinted like text): Back/Forward/Reload are `arrow-left`/`arrow-right`/
+`refresh`, the tab close and a bookmark row's remove `x`, new tab `plus`, the star `star`
+(accent-lit when saved), the bookmarks button `bookmarks`, a select list's current option
+`check`. Never a character standing in for one (the bar was `<` `>` `R` `*` `B`); the
+fallback, drawn only with the icon set missing, is a plain word.
+
 ### Favorites are not bookmarks
 
 Two stores, two meanings. The **star** (`Ctrl+D`, `cce://bookmarks`) is the
@@ -301,18 +338,18 @@ permanent one-click spot: a **strip of label pills inside the bar**, between
 the tab row and the controls row. Click loads the favorite in the active tab
 and folds the bar (a menu pick); middle-click opens it in a background tab
 and leaves the bar out. Insertion order is strip order; the page reorders
-(▲/▼), renames (a GET form per row — form submissions reach the `cce:`
+(cce-icons' arrow-up/arrow-down, inlined as SVG in `pages.rs`), renames (a GET form per row — form submissions reach the `cce:`
 handler like any other navigation) and removes.
 
 ### The bookmarks menu
 
-The controls row's **"B" button** (immediately left of the star) drops the
+The controls row's **bookmarks button** (immediately left of the star) drops the
 bookmarks menu: the star is *this* page's bookmark, the button beside it is
 all of them. A **search field** on top, then three sections — add/remove
 this page, the saved pages themselves (newest first, the `cce://bookmarks`
 order), and `Manage Bookmarks (n)` which hands the collection to that page. A row visits
 in the active tab and folds everything away, middle-click opens it in a
-background tab and leaves the menu up, and the **remove "x"** on the hovered row prunes
+background tab and leaves the menu up, and the **remove glyph** (`x`) on the hovered row prunes
 in place. It closes on Escape (ahead of the URL bar and the page), on a
 click anywhere off its plate, and with the bar it hangs from.
 
@@ -404,7 +441,7 @@ the lerp from the dot's disc up to the bar — and
 any of it shows). The bar's contents are laid out at their *final* rects and
 clipped to the growing plate, so the unfold is a reveal, not a re-layout. The
 row the dot sits on reserves `DOT_COL` at its right end (`dot_col`: the tab
-row's "+" for a top bar, the controls row's star for a bottom one);
+row's new-tab plus for a top bar, the controls row's star for a bottom one);
 `bar_rect` and the rest of the helpers are otherwise unchanged. The plate is
 drawn through `plate_shaped`, a per-plate corner exponent added to `cce-ui`,
 easing from circular at the seed to the DE's own squircle as it becomes the
@@ -472,7 +509,7 @@ Every "open this in a new tab" that is not the person asking for a fresh tab
 opens **behind** the page: a middle-clicked link in a page, a middle-clicked
 favorite or bookmark-menu row, and the context menu's "Open Link in New Tab".
 The active tab, its focus and the URL bar stay as they were, and the bar
-peeks (above). `Ctrl+T`, the "+" and a forwarded external open still open in
+peeks (above). `Ctrl+T`, the new-tab plus and a forwarded external open still open in
 front — those are asks to *go* somewhere.
 
 A page link reaches the chrome through WebKit's `decide-policy`
diff --git a/src/main.rs b/src/main.rs
index ccc9be2..b46cfd3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -127,7 +127,7 @@ const DOT_R: f32 = 1.75 * plate_dock::CORNER_R;
 const DOT_INSET: f32 = plate_dock::CORNER_INSET + (DOT_R - plate_dock::CORNER_R);
 /// Width reserved at the right end of the row the corner control sits on
 /// (the tab row for a top bar, the controls row for a bottom one), so the
-/// "+" or the bookmark star clears the dot in the bar's corner.
+/// new-tab plus or the bookmark star clears the dot in the bar's corner.
 const DOT_COL: f32 = 2.0 * DOT_INSET;
 
 /// The reservation a row makes for the corner control: `DOT_COL` on the
@@ -139,7 +139,7 @@ fn dot_col(position: settings::BarPosition, tabs_row: bool) -> f32 {
 const TAB_H: f32 = 24.0;
 const TAB_MIN_W: f32 = 56.0;
 const TAB_MAX_W: f32 = 200.0;
-/// Tabs at least this wide get a close "x" region on their right edge.
+/// Tabs at least this wide get a close (`x` glyph) region on their right edge.
 const TAB_CLOSE_MIN_W: f32 = 72.0;
 const TAB_CLOSE_W: f32 = 18.0;
 const PLUS_W: f32 = 26.0;
@@ -149,8 +149,8 @@ const PLUS_W: f32 = 26.0;
 const FAV_H: f32 = 22.0;
 const FAV_MAX_W: f32 = 150.0;
 const FAV_FONT: f32 = 12.0;
-/// The bookmarks menu: a plate of rows dropped from the controls row's "B"
-/// button — the bar's own way to visit and manage what the star saves.
+/// The bookmarks menu: a plate of rows dropped from the controls row's
+/// bookmarks button — the bar's own way to visit and manage what the star saves.
 const BM_W: f32 = 320.0;
 const BM_ROW_H: f32 = 24.0;
 /// Height of the rule between the menu's three sections.
@@ -581,7 +581,7 @@ impl OptMenu {
 }
 
 /// The bookmarks menu: the bar's list of saved pages, open under (or over)
-/// the "B" button in the controls row.
+/// the bookmarks button in the controls row.
 ///
 /// It holds a **snapshot** of the store rather than reading it per frame:
 /// the list a pointer is travelling down must not reorder underneath it,
@@ -1108,7 +1108,7 @@ fn tab_rect(bar: &Rect, position: settings::BarPosition, count: usize, i: usize)
     }
 }
 
-/// The close "x" hit region on a tab pill, when the pill is wide enough.
+/// The close hit region on a tab pill, when the pill is wide enough.
 fn tab_close_rect(pill: &Rect) -> Option<Rect> {
     (pill.width >= TAB_CLOSE_MIN_W).then(|| Rect {
         x: pill.x + pill.width - TAB_CLOSE_W,
@@ -2684,6 +2684,34 @@ impl BrowserApp {
         String::new()
     }
 
+    /// A cce-icons glyph `size` px square, centred in `r` and tinted
+    /// `color` as text is — the one way this chrome draws a symbol, never a
+    /// character standing in for one. Should the icon set be missing,
+    /// `fallback` (a plain word, never a symbol) is drawn small in its
+    /// place, cut to the rect.
+    fn glyph(pc: &mut PaintCtx, sans: &str, name: &str, fallback: &str, r: Rect, size: f32, color: [u8; 3]) {
+        let g = Rect {
+            x: r.x + (r.width - size) / 2.0,
+            y: r.y + (r.height - size) / 2.0,
+            width: size,
+            height: size,
+        };
+        let c = color.map(|v| v as f32 / 255.0);
+        if pc.icon(name, g, [c[0], c[1], c[2], 1.0]) {
+            return;
+        }
+        const FALLBACK_FONT: f32 = 9.0;
+        let word = Self::fit_text(fallback, sans, FALLBACK_FONT, r.width - 2.0);
+        let w = measure_text_width(&word, sans, FALLBACK_FONT);
+        pc.text(
+            word,
+            r.x + (r.width - w) / 2.0,
+            cce_ui::layout::align_text_y(r.y, r.height, FALLBACK_FONT, 0.0),
+            FALLBACK_FONT,
+            color,
+        );
+    }
+
     /// Draw the page-blocking prompt, if one is up. Same primitives as the
     /// utility bar — there are no cce-ui widgets in this app — with a scrim
     /// over the page so it reads as blocked, which it genuinely is.
@@ -2989,16 +3017,18 @@ impl BrowserApp {
                 // saved page is available. Hover is the highlight's job.
                 TEXT,
             );
-            // The remove "x" shows on the hovered row only — always-on x's
+            // The remove glyph shows on the hovered row only — always-on x's
             // down a whole list read as clutter, and as a hazard.
             if hovered {
                 let on_rm = hover == Some(BmHit::Entry(*i, true));
-                let xw = measure_text_width("x", sans, 11.0);
-                pc.text(
+                let rm = Rect { x: r.x + r.width - BM_RM_W, width: BM_RM_W, ..*r };
+                Self::glyph(
+                    pc,
+                    sans,
                     "x",
-                    r.x + r.width - BM_RM_W / 2.0 - xw / 2.0,
-                    cce_ui::layout::align_text_y(r.y, r.height, 11.0, 0.0),
-                    11.0,
+                    "Remove",
+                    rm,
+                    10.0,
                     if on_rm { [212, 155, 155] } else { TEXT_DIM },
                 );
             }
@@ -3057,7 +3087,7 @@ impl BrowserApp {
     }
 
     /// Draw a select's list: its options, the current value marked with a
-    /// dot, optgroup headings dim and their options indented under them.
+    /// check glyph, optgroup headings dim and their options indented under them.
     #[cfg(feature = "wpe")]
     fn paint_opt_menu(&mut self, pc: &mut PaintCtx, sans: &str) {
         let Some(l) = self.opt_layout() else { return };
@@ -3077,8 +3107,8 @@ impl BrowserApp {
             }
             let gutter = r.x + text_pad();
             if it.selected {
-                let t = TEXT.map(|c| c as f32 / 255.0);
-                pc.circle(gutter + OPT_INDENT / 2.0 - 2.0, r.y + r.height / 2.0, 2.5, [t[0], t[1], t[2], 1.0]);
+                let mark = Rect { x: gutter - 2.0, y: r.y, width: OPT_INDENT, height: r.height };
+                Self::glyph(pc, sans, "check", "", mark, 10.0, TEXT);
             }
             let x = gutter + OPT_INDENT + if it.group_child { OPT_INDENT } else { 0.0 };
             let avail = r.x + r.width - x - text_pad() - if scrolls { 6.0 } else { 0.0 };
@@ -5262,26 +5292,13 @@ impl Application for BrowserApp {
                     );
                 }
                 if let Some(cr) = close {
-                    let xw = measure_text_width("x", &sans, 11.0);
-                    pc.text(
-                        "x",
-                        cr.x + (cr.width - xw) / 2.0 - 2.0,
-                        cce_ui::layout::align_text_y(cr.y, cr.height, 11.0, 0.0),
-                        11.0,
-                        TEXT_DIM,
-                    );
+                    let glyph_box = Rect { x: cr.x - 2.0, ..cr };
+                    Self::glyph(pc, &sans, "x", "Close", glyph_box, 9.0, TEXT_DIM);
                 }
             }
             let plus = plus_rect(&bar, pos_edge);
             pc.rounded_rect(plus, 7.0, (true, true, true, true), BTN_BG);
-            let pw = measure_text_width("+", &sans, 14.0);
-            pc.text(
-                "+",
-                plus.x + (plus.width - pw) / 2.0,
-                cce_ui::layout::align_text_y(plus.y, plus.height, 14.0, 0.0),
-                14.0,
-                TEXT,
-            );
+            Self::glyph(pc, &sans, "plus", "New", plus, 12.0, TEXT);
 
             // Favorites strip: label pills, the hovered one lifted like an
             // active tab. Labels are cut to the pill, never the other way.
@@ -5305,21 +5322,14 @@ impl Application for BrowserApp {
                 );
             }
 
-            let labels = ["<", ">", "R"];
+            // (glyph, fallback word) for Back, Forward and Reload.
+            let faces = [("arrow-left", "Back"), ("arrow-right", "Forward"), ("refresh", "Reload")];
             let enabled = [self.host.can_go_back(), self.host.can_go_forward(), true];
-            for (i, label) in labels.iter().enumerate() {
+            for (i, (name, word)) in faces.iter().enumerate() {
                 let r = btn_rect(&bar, i);
                 pc.rounded_rect(r, 6.0, (true, true, true, true), BTN_BG);
                 let color = if enabled[i] { TEXT } else { TEXT_DIM };
-                let (sans, ..) = cce_ui::layout::read_preferred_fonts();
-                let lw = measure_text_width(label, &sans, 14.0);
-                pc.text(
-                    *label,
-                    r.x + (r.width - lw) / 2.0,
-                    cce_ui::layout::align_text_y(r.y, r.height, 14.0, 0.0),
-                    14.0,
-                    color,
-                );
+                Self::glyph(pc, &sans, name, word, r, 14.0, color);
             }
 
             // Bookmark star: accent-lit when the page is bookmarked.
@@ -5327,27 +5337,14 @@ impl Application for BrowserApp {
             pc.rounded_rect(star, 6.0, (true, true, true, true), BTN_BG);
             let starred = self.host.active_bookmarked();
             let star_color: [u8; 3] = if starred { [150, 190, 240] } else { TEXT_DIM };
-            let sw = measure_text_width("*", &sans, 17.0);
-            pc.text(
-                "*",
-                star.x + (star.width - sw) / 2.0,
-                cce_ui::layout::align_text_y(star.y, star.height, 17.0, 0.0) + 3.0,
-                17.0,
-                star_color,
-            );
+            Self::glyph(pc, &sans, "star", "Save", star, 15.0, star_color);
 
             // Bookmarks menu button: all the saved pages, where the star
             // beside it is only this one. Lit while its menu is open.
             let bmb = bm_btn_rect(&bar, pos_edge);
             pc.rounded_rect(bmb, 6.0, (true, true, true, true), BTN_BG);
-            let bw = measure_text_width("B", &sans, 14.0);
-            pc.text(
-                "B",
-                bmb.x + (bmb.width - bw) / 2.0,
-                cce_ui::layout::align_text_y(bmb.y, bmb.height, 14.0, 0.0),
-                14.0,
-                if self.bm_menu.is_some() { [150, 190, 240] } else { TEXT },
-            );
+            let bm_color = if self.bm_menu.is_some() { [150, 190, 240] } else { TEXT };
+            Self::glyph(pc, &sans, "bookmarks", "Saved", bmb, 14.0, bm_color);
 
             // URL field: rim + recess, brighter rim when focused.
             let f = url_rect(&bar, pos_edge);
diff --git a/src/pages.rs b/src/pages.rs
index 280c8d1..5f096e7 100644
--- a/src/pages.rs
+++ b/src/pages.rs
@@ -415,6 +415,26 @@ fn default_label(url: &str, title: &str) -> String {
     host.or(file).unwrap_or_else(|| url.to_string())
 }
 
+/// The favorites page's move-up mark: cce-icons' `svg/arrow-up.svg`, copied
+/// inline (an `include_str!` of the sibling crate would break a standalone
+/// clone) with its `#ffffff` made `currentColor`, so it takes the link's
+/// colour — dim, the hover red, the disabled end's — as the text did.
+const ARROW_UP_SVG: &str = "<svg viewBox=\"0 0 40 40\" width=\"1em\" height=\"1em\" \
+     style=\"vertical-align:-.15em\" aria-label=\"up\" xmlns=\"http://www.w3.org/2000/svg\">\
+     <g fill=\"currentColor\" transform=\"rotate(-90 20 20)\">\
+     <rect x=\"4\" y=\"17\" width=\"18\" height=\"6\" rx=\"3\"/>\
+     <path d=\"M22 11 L33 20 L22 29 Z\" stroke=\"currentColor\" stroke-width=\"5\" stroke-linejoin=\"round\"/>\
+     </g></svg>";
+
+/// The move-down mark: cce-icons' `svg/arrow-down.svg`, copied as
+/// [`ARROW_UP_SVG`] is.
+const ARROW_DOWN_SVG: &str = "<svg viewBox=\"0 0 40 40\" width=\"1em\" height=\"1em\" \
+     style=\"vertical-align:-.15em\" aria-label=\"down\" xmlns=\"http://www.w3.org/2000/svg\">\
+     <g fill=\"currentColor\" transform=\"rotate(90 20 20)\">\
+     <rect x=\"4\" y=\"17\" width=\"18\" height=\"6\" rx=\"3\"/>\
+     <path d=\"M22 11 L33 20 L22 29 Z\" stroke=\"currentColor\" stroke-width=\"5\" stroke-linejoin=\"round\"/>\
+     </g></svg>";
+
 /// The favorites: a short, ordered, hand-curated list of places, shown as a
 /// row of pills in the utility bar. Deliberately not the bookmarks — the
 /// star is an archive of everything worth finding again; this is the
@@ -511,14 +531,14 @@ impl Favorites {
             let label = default_label(&e.url, &e.title);
             // Ordering links; the end pill has nowhere further to go.
             let up = if i > 0 {
-                format!("<a class=rm href=\"cce://favorites/up?url={}\">&#9650;</a>", html_escape(&enc))
+                format!("<a class=rm href=\"cce://favorites/up?url={}\">{ARROW_UP_SVG}</a>", html_escape(&enc))
             } else {
-                "<span class=rm>&#9650;</span>".to_string()
+                format!("<span class=rm>{ARROW_UP_SVG}</span>")
             };
             let down = if i < last {
-                format!("<a class=rm href=\"cce://favorites/down?url={}\">&#9660;</a>", html_escape(&enc))
+                format!("<a class=rm href=\"cce://favorites/down?url={}\">{ARROW_DOWN_SVG}</a>", html_escape(&enc))
             } else {
-                "<span class=rm>&#9660;</span>".to_string()
+                format!("<span class=rm>{ARROW_DOWN_SVG}</span>")
             };
             rows.push_str(&format!(
                 "<div class=e><span class=w>{up} {down}</span>\