GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(menu): draw context-menu labels in the menu font
The shared context menu hardcoded a 12.0 label size, left the family
unset, and sized its plate from `bytes * 7.5`. None of the three is
the DE's menu font, so a menu opened over a Berkeley Mono list read in
the default sans, and a wide mono header — a breadcrumb's full path —
outran a plate measured for a narrower face.
`label_font()` is now the one source for that face: `menubar_font`,
family and size, the same font the menubar's own drop-downs use.
Widths come from the shaped widest label rather than a byte count (so
a non-ASCII entry no longer over-measures), and the label run carries
the resolved size.
A TextLabel has no room for a family, so every consumer that paired
`paint()` with its own `text_labels()` loop passed None and undid the
family half. `paint_with_labels()` does both in one call and cannot
forget it; the pair stays for callers that need their own bounds.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/widget/core.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 63 insertions(+), 6 deletions(-)
diff --git a/src/widget/core.rs b/src/widget/core.rs
index a2ba8b1..8128dcf 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -337,8 +337,18 @@ pub mod context_menu {
/// are drawn selects the entry above the one under the cursor.
pub const ROW_H: f32 = 24.0;
- /// Point size of a menu label.
- const LABEL_SIZE: f32 = 12.0;
+ /// The face a menu label is drawn in: the DE's menu font, family and
+ /// size — the same `menubar_font` the menubar's own drop-downs use
+ /// ([`crate::widget::container::menu`]). A menu that hardcodes 12.0 and
+ /// leaves the family unset renders in the default sans while the list or
+ /// breadcrumb beneath it wears the configured face.
+ ///
+ /// Consumers need the family too: a [`TextLabel`] carries only a size, so
+ /// whoever turns these labels into text prims passes this family alongside
+ /// them (`pc.text_with(.., Some(family), ..)`).
+ pub fn label_font() -> (String, f32) {
+ crate::layout::menubar_font_parsed()
+ }
/// A toolkit color as the `[u8; 3]` a [`TextLabel`] carries.
fn rgb8(c: [f32; 4]) -> [u8; 3] {
@@ -384,8 +394,17 @@ pub mod context_menu {
self.y = y;
self.options = options;
self.h = self.options.len() as f32 * ROW_H;
- let max_len = self.options.iter().map(|s| s.len()).max().unwrap_or(0);
- self.w = ((max_len as f32 * 7.5) + 24.0).max(120.0);
+ // Width from the SHAPED widest label, not `bytes * 7.5`: that
+ // estimate is a different face's advance (and counts bytes, so a
+ // non-ASCII label over-measures), which is how a menu ends up
+ // either clipping its longest entry or padded out past it.
+ let (family, size) = label_font();
+ let widest = self
+ .options
+ .iter()
+ .map(|s| crate::widget::display::measure_text_width(s, &family, size))
+ .fold(0.0f32, f32::max);
+ self.w = (widest + 24.0).max(120.0);
self.visible = true;
self.hovered_item = None;
self.target = Some(target);
@@ -580,6 +599,37 @@ pub mod context_menu {
quads
}
+ /// [`paint`](Self::paint) plus the label run, in the menu font.
+ ///
+ /// A [`TextLabel`] carries a size but no family, so a consumer that
+ /// hand-rolls `paint()` + a `text_labels()` loop has to remember to
+ /// pass [`label_font`]'s family itself — and every one of them passed
+ /// `None`, which is why menus rendered in the default sans over lists
+ /// wearing the configured face. This is the call that cannot forget
+ /// it; prefer it over the pair.
+ pub fn paint_with_labels(&self, ctx: &mut crate::scene::paint::PaintCtx) {
+ self.paint(ctx);
+ if !self.visible {
+ return;
+ }
+ let (family, _) = label_font();
+ // The menu's own rect: the engine's popover clamp exempts exactly
+ // these bounds, so the labels render inside the plate instead of
+ // being clipped to the page content beneath it.
+ let bounds = Some([self.x, self.y, self.x + self.w, self.y + self.h]);
+ for label in self.text_labels() {
+ ctx.text_with(
+ label.text,
+ label.x,
+ label.y,
+ label.font_size,
+ label.color,
+ Some(family.clone()),
+ bounds,
+ );
+ }
+ }
+
pub fn text_labels(&self) -> Vec<TextLabel> {
let mut labels = Vec::new();
if !self.visible { return labels; }
@@ -588,7 +638,8 @@ pub mod context_menu {
if opt == "-" {
continue;
}
- let iy = self.y + idx as f32 * ROW_H + (ROW_H - LABEL_SIZE) / 2.0;
+ let (_, label_size) = label_font();
+ let iy = self.y + idx as f32 * ROW_H + (ROW_H - label_size) / 2.0;
// The toolkit's semantic colors rather than greys hand-mixed
// against the old near-black fill: on the plate's mid-slate the
// header's 0x70 was a step above its background and read as
@@ -605,7 +656,7 @@ pub mod context_menu {
text: opt.clone(),
x: self.x + 8.0,
y: iy,
- font_size: 12.0,
+ font_size: label_size,
color: text_color,
});
}
@@ -672,6 +723,12 @@ pub mod context_menu {
pub fn text_labels() -> Vec<TextLabel> {
CONTEXT_MENU.with(|m| m.borrow().text_labels())
}
+
+ /// Plate and labels in one call — see
+ /// [`ContextMenuState::paint_with_labels`].
+ pub fn paint_with_labels(ctx: &mut crate::scene::paint::PaintCtx) {
+ CONTEXT_MENU.with(|m| m.borrow().paint_with_labels(ctx));
+ }
}
#[derive(Debug, Clone)]