web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
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={}\">▲</a>", html_escape(&enc))
+ format!("<a class=rm href=\"cce://favorites/up?url={}\">{ARROW_UP_SVG}</a>", html_escape(&enc))
} else {
- "<span class=rm>▲</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={}\">▼</a>", html_escape(&enc))
+ format!("<a class=rm href=\"cce://favorites/down?url={}\">{ARROW_DOWN_SVG}</a>", html_escape(&enc))
} else {
- "<span class=rm>▼</span>".to_string()
+ format!("<span class=rm>{ARROW_DOWN_SVG}</span>")
};
rows.push_str(&format!(
"<div class=e><span class=w>{up} {down}</span>\