web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
feat: the utility bar is a circle menu — an orb that unfolds into the bar
The chrome now rests as a 40px frosted orb in the bar's corner (nearest
the anchored edge) and unfolds into the existing two-row bar on demand:
click the orb, Ctrl+L, or Ctrl+T open it; a page click, Escape,
submitting a URL, or picking a tab fold it back. The morph is a lerp of
the plate between the two shapes over 180ms with the bar's contents laid
out at their final rects and clipped to the growing plate; the corner
exponent eases from 2 (a true circle, via cce-ui's new `plate_shaped`)
to the DE's own so the open bar matches its neighbours. Draw and
hit-test both read `chrome_plate()` / `chrome_hit()`; `bar_rect` and the
helpers under it are untouched. While loading the orb wears an accent
ring where the bar wears its strip.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 31 +++++
src/main.rs | 431 +++++++++++++++++++++++++++++++++++++++++-------------------
2 files changed, 325 insertions(+), 137 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 5623a6b..db5b0ca 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -169,6 +169,37 @@ every hit test in `handle_mouse_input` re-derives the same rects from the same
`bar_rect`/`tab_rect`/`btn_rect`/`url_rect` helpers. **Draw and hit-test are two
readings of one geometry** — change a rect helper, not one call site.
+### The bar is a circle menu
+
+The chrome rests as a **40px orb** (`ORB_D`) in the bar's corner nearest the
+anchored window edge and unfolds into the two-row bar on demand; `chrome_open`
+names the state and `chrome_t` is the unfold progress, animated in `tick` over
+`CHROME_ANIM_S`. `chrome_plate()` is the one shape both draw and hit-test read
+— the orb, the bar, or the lerp between them — and `chrome_hit()` is the
+chrome's pointer gate (a circle-distance test when closed, so the orb's corner
+pixels belong to the page). The bar's contents are laid out at their *final*
+rects and clipped to the growing plate, which is what makes the unfold a reveal
+rather than a re-layout; `bar_rect` and every helper under it are unchanged.
+
+The orb is drawn through `plate_shaped(.., Some(2.0))`, a per-plate corner
+exponent added to `cce-ui` for exactly this: the DE runs `corner_shape 4.5`, and
+at half-extent radii that squircle is a rounded square, not a circle. The
+exponent eases back to the DE's as the plate becomes the bar, so the open bar's
+corners match its neighbours.
+
+Menu semantics, all in `handle_mouse_input` / `handle_key_input`:
+
+- **Open**: click the orb; `Ctrl+L` (then focuses the URL); `Ctrl+T` (a new
+ tab focuses the URL field, which must be on screen).
+- **Fold**: click the page; `Escape` with the URL unfocused (the first Escape
+ in a focused field only drops focus, as before); submitting a URL; picking a
+ tab. Closing a tab does *not* fold — several often go in a row.
+- Folding drops URL-bar focus (`close_chrome`), so an off-screen field never
+ keeps eating keystrokes. Wheel and pointer moves over the chrome stay off the
+ page, gated by `chrome_hit`, not `bar_rect`.
+- While loading, the orb wears an accent ring where the bar wears its bottom
+ strip; both fade across the morph.
+
- Everything bar-relative derives from `bar_rect`, never from `BAR_MARGIN` directly,
or the bar-position setting silently stops moving things to the bottom edge.
- `BAR_FILL`'s **negative alpha is the frost sentinel** (`cce-ui/src/scene/paint.rs`):
diff --git a/src/main.rs b/src/main.rs
index acef84c..8ead264 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,9 +2,11 @@
//!
//! Servo renders pages into a CPU (software) rendering context; each
//! finished frame is read back and uploaded to cce-ui's image registry,
-//! then drawn as a single quad under a thin chrome bar (back / forward /
-//! reload / URL field). Input over the page area is translated into Servo
-//! input events; the URL bar is a small hand-rolled line editor.
+//! then drawn as a single quad under the chrome: a circle menu that rests
+//! as a small orb in the window corner and opens into the utility bar
+//! (tabs, back / forward / reload, URL field). Input over the page area is
+//! translated into Servo input events; the URL bar is a small hand-rolled
+//! line editor.
mod downloads;
mod instance;
@@ -54,6 +56,15 @@ const BAR_MARGIN: f32 = 10.0;
const BAR_H: f32 = BAR_PAD + TAB_H + ROW_GAP + BTN_H + BAR_PAD;
const BAR_RADIUS: f32 = 10.0;
const BAR_PAD: f32 = 7.0;
+/// Diameter of the collapsed chrome — the orb the bar folds down to. One
+/// control's height plus the bar padding, so the closed and open plates
+/// share an edge thickness.
+const ORB_D: f32 = BAR_PAD + BTN_H + BAR_PAD;
+/// Seconds for the orb to unfold into the bar (and back).
+const CHROME_ANIM_S: f32 = 0.18;
+/// Hamburger glyph inside the orb: line length and spacing.
+const ORB_GLYPH_W: f32 = 14.0;
+const ORB_GLYPH_GAP: f32 = 4.5;
const TAB_H: f32 = 24.0;
const TAB_GAP: f32 = 4.0;
const TAB_MIN_W: f32 = 56.0;
@@ -263,6 +274,11 @@ struct BrowserApp {
/// one the dialog fields use.
url: lineedit::LineEdit,
url_focused: bool,
+ /// The circle menu: closed is the orb, open is the full utility bar.
+ /// `chrome_t` is the unfold progress (0 = orb, 1 = bar), animated in
+ /// `tick` toward whichever state `chrome_open` names.
+ chrome_open: bool,
+ chrome_t: f32,
loading: bool,
/// Page title; drives the toplevel title (the engine re-applies
/// `settings().title` whenever it changes).
@@ -444,6 +460,67 @@ impl BrowserApp {
bar_rect(self.win, self.settings.bar_position)
}
+ /// The closed chrome: a circle in the bar's corner nearest the anchored
+ /// window edge, so the bar unfolds away from it and the orb never moves.
+ fn orb_rect(&self) -> Rect {
+ let bar = self.bar();
+ let y = match self.settings.bar_position {
+ settings::BarPosition::Top => bar.y,
+ settings::BarPosition::Bottom => bar.y + bar.height - ORB_D,
+ };
+ Rect { x: bar.x, y, width: ORB_D, height: ORB_D }
+ }
+
+ /// Unfold progress with easing applied — what the plate is drawn from.
+ fn chrome_ease(&self) -> f32 {
+ let t = self.chrome_t.clamp(0.0, 1.0);
+ t * t * (3.0 - 2.0 * t)
+ }
+
+ /// The chrome plate as currently drawn — the orb, the bar, or the
+ /// in-between shape while it unfolds — and its corner radius.
+ fn chrome_plate(&self) -> (Rect, f32) {
+ let e = self.chrome_ease();
+ let orb = self.orb_rect();
+ let bar = self.bar();
+ let lerp = |a: f32, b: f32| a + (b - a) * e;
+ let plate = Rect {
+ x: lerp(orb.x, bar.x),
+ y: lerp(orb.y, bar.y),
+ width: lerp(orb.width, bar.width),
+ height: lerp(orb.height, bar.height),
+ };
+ (plate, lerp(ORB_D / 2.0, BAR_RADIUS))
+ }
+
+ /// Whether a pointer position is over the chrome — the circle proper
+ /// when closed (its corners belong to the page), the plate otherwise.
+ fn chrome_hit(&self, x: f32, y: f32) -> bool {
+ if self.chrome_t <= 0.0 {
+ let o = self.orb_rect();
+ let (cx, cy) = (o.x + o.width / 2.0, o.y + o.height / 2.0);
+ let r = o.width / 2.0;
+ (x - cx) * (x - cx) + (y - cy) * (y - cy) <= r * r
+ } else {
+ hit(&self.chrome_plate().0, x, y)
+ }
+ }
+
+ fn open_chrome(&mut self) {
+ self.chrome_open = true;
+ }
+
+ /// Fold the bar back into the orb; drops URL-bar focus with it, since
+ /// a field that is not on screen must not keep eating keystrokes.
+ fn close_chrome(&mut self) {
+ self.chrome_open = false;
+ if self.url_focused {
+ self.url_focused = false;
+ self.url.selection = None;
+ self.sync_page_state();
+ }
+ }
+
/// The page fills the whole window; the utility bar floats above it.
fn content_px(&self) -> (u32, u32) {
(
@@ -608,6 +685,8 @@ impl BrowserApp {
self.url_focused = false;
self.url.selection = None;
self.loading = true;
+ // Submitting is the menu's "pick": it folds away to show the page.
+ self.chrome_open = false;
}
}
@@ -692,6 +771,9 @@ impl BrowserApp {
self.host.open_tab(url);
self.url = lineedit::LineEdit::default();
self.url_focused = true;
+ // The focused field has to be on screen, so a new tab unfolds the
+ // menu even when it was opened by chord.
+ self.open_chrome();
self.sync_page_state();
self.persist_session();
}
@@ -980,6 +1062,8 @@ impl Application for BrowserApp {
pointer: (0.0, 0.0),
url: lineedit::LineEdit::with_text(url_text),
url_focused: false,
+ chrome_open: false,
+ chrome_t: 0.0,
loading: true,
title: None,
#[cfg(feature = "wpe")]
@@ -1106,7 +1190,19 @@ impl Application for BrowserApp {
}
}
- fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
+ fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+ let target = if self.chrome_open { 1.0 } else { 0.0 };
+ if self.chrome_t != target {
+ let step = dt / CHROME_ANIM_S;
+ self.chrome_t = if target > self.chrome_t {
+ (self.chrome_t + step).min(1.0)
+ } else {
+ (self.chrome_t - step).max(0.0)
+ };
+ // Keeps the runner's warm loop alive until the morph lands.
+ *needs_rebuild = true;
+ }
+ }
fn handle_focus_change(&mut self, focused: bool, needs_rebuild: &mut bool) {
// A settings change can move the bar to the other edge, so a reload
@@ -1132,7 +1228,7 @@ impl Application for BrowserApp {
*_needs_rebuild = true;
return;
}
- if !hit(&self.bar(), pos.x, pos.y) {
+ if !self.chrome_hit(pos.x, pos.y) {
let s = self.scale as f32;
self.host.mouse_move(pos.x * s, pos.y * s);
}
@@ -1190,11 +1286,19 @@ impl Application for BrowserApp {
}
let bar = self.bar();
- if hit(&bar, pos.x, pos.y) {
+ if self.chrome_hit(pos.x, pos.y) {
if !pressed || !matches!(button, MouseButton::Left | MouseButton::Middle) {
return None;
}
*needs_rebuild = true;
+ // Closed (or still folding): the whole orb is the open control.
+ // Anything folding open is treated as the bar it is becoming.
+ if !self.chrome_open {
+ if button == MouseButton::Left {
+ self.open_chrome();
+ }
+ return None;
+ }
// Tab strip: activate / close (x region or middle click) / new tab.
let count = self.host.tab_count();
for i in 0..count {
@@ -1208,6 +1312,9 @@ impl Application for BrowserApp {
return self.close_tab(i);
}
self.switch_tab(i);
+ // Picking a tab is a menu choice: the bar folds away. Closing
+ // one is not — several may go in a row.
+ self.close_chrome();
return None;
}
if button != MouseButton::Left {
@@ -1243,11 +1350,10 @@ impl Application for BrowserApp {
return None;
}
- // Page area: a click dismisses URL-bar focus, then goes to the page.
- if self.url_focused && pressed {
- self.url_focused = false;
- self.url.selection = None;
- self.sync_page_state();
+ // Page area: a click folds the menu (and URL-bar focus with it),
+ // then goes to the page.
+ if self.chrome_open && pressed {
+ self.close_chrome();
*needs_rebuild = true;
}
match button {
@@ -1263,7 +1369,7 @@ impl Application for BrowserApp {
}
fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, _needs_rebuild: &mut bool) {
- if hit(&self.bar(), pos.x, pos.y) {
+ if self.chrome_hit(pos.x, pos.y) {
return;
}
// WheelDelta keeps cce-ui's winit sign convention (positive = scroll
@@ -1405,6 +1511,7 @@ impl Application for BrowserApp {
return None;
}
"l" => {
+ self.open_chrome();
self.url_focused = true;
self.select_all_url();
*needs_rebuild = true;
@@ -1422,6 +1529,12 @@ impl Application for BrowserApp {
self.host.reload();
return None;
}
+ // An open menu owns Escape; closed, the page keeps it.
+ if self.chrome_open && event.logical_key == Key::Named(NamedKey::Escape) {
+ self.close_chrome();
+ *needs_rebuild = true;
+ return None;
+ }
}
self.host.key_ui(event);
@@ -1449,144 +1562,188 @@ impl Application for BrowserApp {
pc.text("Loading...", BAR_MARGIN + 6.0, y, 13.0, TEXT_DIM);
}
- // Floating utility bar: a blur-behind plate frosting the page under it.
- let radii = (BAR_RADIUS, BAR_RADIUS, BAR_RADIUS, BAR_RADIUS);
- pc.plate(bar, radii, BAR_FILL, cce_ui::layout::bevel_width().min(4.0));
- if self.loading {
- pc.clip_rounded(bar, BAR_RADIUS, |pc| {
- pc.quad(
- Rect { x: bar.x, y: bar.y + bar.height - 2.0, width: bar.width, height: 2.0 },
- ACCENT,
+ // The chrome plate: the orb, the bar, or the shape between them
+ // while it unfolds. Blur-behind either way, frosting the page.
+ let e = self.chrome_ease();
+ let (plate, radius) = self.chrome_plate();
+ // A circle when closed — exponent 2 with half-extent radii — easing
+ // into the DE's own corner shape as it becomes the bar, so the orb is
+ // round even in a squircle DE and the open bar matches its neighbours.
+ let shape = 2.0 + (cce_ui::layout::corner_shape() - 2.0) * e;
+ pc.plate_shaped(
+ plate,
+ (radius, radius, radius, radius),
+ BAR_FILL,
+ cce_ui::layout::bevel_width().min(4.0),
+ Some(shape),
+ );
+ let (sans, ..) = cce_ui::layout::read_preferred_fonts();
+
+ // Closed: the orb carries a menu glyph and, while a page loads, an
+ // accent ring — the collapsed reading of the bar's loading strip.
+ // Both fade out as the bar unfolds over them.
+ if e < 1.0 {
+ let o = self.orb_rect();
+ let (cx, cy) = (o.x + o.width / 2.0, o.y + o.height / 2.0);
+ let fade = 1.0 - e;
+ let glyph = [0.86, 0.87, 0.90, 0.95 * fade];
+ for k in -1..=1 {
+ let y = cy + k as f32 * ORB_GLYPH_GAP;
+ pc.vector(
+ cx - ORB_GLYPH_W / 2.0,
+ y,
+ cx + ORB_GLYPH_W / 2.0,
+ y,
+ 1.6,
+ glyph,
+ cce_ui::scene::paint::Cap::Round,
);
- });
+ }
+ if self.loading {
+ let ring = [ACCENT[0], ACCENT[1], ACCENT[2], fade];
+ pc.arc(cx, cy, ORB_D / 2.0 - 2.0, 2.0, 0.0, std::f32::consts::TAU, ring);
+ }
}
- // Tab strip.
- let (sans, ..) = cce_ui::layout::read_preferred_fonts();
- let count = self.host.tab_count();
- let active = self.host.active_index();
- for i in 0..count {
- let pill = tab_rect(&bar, count, i);
- let is_active = i == active;
- pc.rounded_rect(
- pill,
- 7.0,
- (true, true, true, true),
- if is_active { TAB_ACTIVE_BG } else { TAB_BG },
- );
- let tab = self.host.tab(i);
- let title = tab
- .and_then(|t| t.title.clone().filter(|s| !s.is_empty()))
- .or_else(|| tab.and_then(|t| t.url.clone()).map(|u| u.to_string()))
- .filter(|s| s != "about:blank")
- .unwrap_or_else(|| "New Tab".to_string());
- let close = tab_close_rect(&pill);
- let text_avail = pill.width - 16.0 - close.map_or(0.0, |_| TAB_CLOSE_W - 4.0);
- let label = Self::fit_text(&title, &sans, 12.0, text_avail);
- let color = if is_active { TEXT } else { TEXT_DIM };
- pc.text(
- label,
- pill.x + 8.0,
- cce_ui::layout::align_text_y(pill.y, pill.height, 12.0, 0.0),
- 12.0,
- color,
- );
- if tab.is_some_and(|t| t.loading) {
- pc.quad(
- Rect { x: pill.x, y: pill.y + pill.height - 2.0, width: pill.width, height: 2.0 },
- ACCENT,
+ // Open: the bar's contents, laid out at their final positions and
+ // clipped to the plate, so they are revealed as it unfolds.
+ if e > 0.0 {
+ pc.clip_rounded(plate, radius, |pc| {
+ if self.loading {
+ pc.quad(
+ Rect { x: bar.x, y: bar.y + bar.height - 2.0, width: bar.width, height: 2.0 },
+ ACCENT,
+ );
+ }
+
+ // Tab strip.
+ let count = self.host.tab_count();
+ let active = self.host.active_index();
+ for i in 0..count {
+ let pill = tab_rect(&bar, count, i);
+ let is_active = i == active;
+ pc.rounded_rect(
+ pill,
+ 7.0,
+ (true, true, true, true),
+ if is_active { TAB_ACTIVE_BG } else { TAB_BG },
);
- }
- if let Some(cr) = close {
- let xw = measure_text_width("x", &sans, 11.0);
+ let tab = self.host.tab(i);
+ let title = tab
+ .and_then(|t| t.title.clone().filter(|s| !s.is_empty()))
+ .or_else(|| tab.and_then(|t| t.url.clone()).map(|u| u.to_string()))
+ .filter(|s| s != "about:blank")
+ .unwrap_or_else(|| "New Tab".to_string());
+ let close = tab_close_rect(&pill);
+ let text_avail = pill.width - 16.0 - close.map_or(0.0, |_| TAB_CLOSE_W - 4.0);
+ let label = Self::fit_text(&title, &sans, 12.0, text_avail);
+ let color = if is_active { TEXT } else { TEXT_DIM };
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,
+ label,
+ pill.x + 8.0,
+ cce_ui::layout::align_text_y(pill.y, pill.height, 12.0, 0.0),
+ 12.0,
+ color,
);
+ if tab.is_some_and(|t| t.loading) {
+ pc.quad(
+ Rect { x: pill.x, y: pill.y + pill.height - 2.0, width: pill.width, height: 2.0 },
+ ACCENT,
+ );
+ }
+ 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 plus = plus_rect(&bar);
- 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,
- );
-
- let labels = ["<", ">", "R"];
- let enabled = [self.host.can_go_back(), self.host.can_go_forward(), true];
- for (i, label) in labels.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);
+ let plus = plus_rect(&bar);
+ pc.rounded_rect(plus, 7.0, (true, true, true, true), BTN_BG);
+ let pw = measure_text_width("+", &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),
+ "+",
+ plus.x + (plus.width - pw) / 2.0,
+ cce_ui::layout::align_text_y(plus.y, plus.height, 14.0, 0.0),
14.0,
- color,
+ TEXT,
);
- }
-
- // Bookmark star: accent-lit when the page is bookmarked.
- let star = star_rect(&bar);
- 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,
- );
- // URL field: rim + recess, brighter rim when focused.
- let f = url_rect(&bar);
- let rim = if self.url_focused { RIM_FOCUS } else { RIM };
- pc.rounded_rect(
- Rect { x: f.x - 1.0, y: f.y - 1.0, width: f.width + 2.0, height: f.height + 2.0 },
- 7.0,
- (true, true, true, true),
- rim,
- );
- 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
- .url
- .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.text.clone(), f.x + URL_PAD_X, ty, URL_FONT, TEXT);
- if let Some(offset) = caret_x {
- pc.quad(
- Rect { x: f.x + URL_PAD_X + offset, y: f.y + 4.0, width: 1.0, height: f.height - 8.0 },
- [0.85, 0.87, 0.92, 1.0],
+ let labels = ["<", ">", "R"];
+ let enabled = [self.host.can_go_back(), self.host.can_go_forward(), true];
+ for (i, label) in labels.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,
);
}
- });
+
+ // Bookmark star: accent-lit when the page is bookmarked.
+ let star = star_rect(&bar);
+ 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,
+ );
+
+ // URL field: rim + recess, brighter rim when focused.
+ let f = url_rect(&bar);
+ let rim = if self.url_focused { RIM_FOCUS } else { RIM };
+ pc.rounded_rect(
+ Rect { x: f.x - 1.0, y: f.y - 1.0, width: f.width + 2.0, height: f.height + 2.0 },
+ 7.0,
+ (true, true, true, true),
+ rim,
+ );
+ 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
+ .url
+ .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.text.clone(), f.x + URL_PAD_X, ty, URL_FONT, TEXT);
+ if let Some(offset) = caret_x {
+ pc.quad(
+ Rect { x: f.x + URL_PAD_X + offset, y: f.y + 4.0, width: 1.0, height: f.height - 8.0 },
+ [0.85, 0.87, 0.92, 1.0],
+ );
+ }
+ });
+
+ });
+ }
#[cfg(feature = "wpe")]
self.paint_ctx_menu(&mut pc, &sans);