git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit078ade372eb6c7523df654f0cfc2cfb748dae738
parent1c47e8dccc
authorLucas Galante <[email protected]>
date2026-07-09 13:03
feat(scene): TextAttrs — italic/weight on display-list text prims

Prim::Text gains attrs: TextAttrs { italic, weight } (toolkit-plain, no
glyphon types in the scene layer; weight is the OpenType value).
PaintCtx::text_attrs emits them (text/text_with default to no attrs);
get_text_buffer_attrs applies them at shape time (get_text_buffer
delegates; the buffer cache key includes them so style variants don't
collide). The dl-text mapping shapes prims through the attrs-aware
entry. Motivated by cce-fonts' style-variant previews — the last thing
its text needed that prims couldn't express.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe

 src/backend/window_runner.rs | 24 ++++++++++++++++++++++--
 src/scene/paint.rs           | 34 +++++++++++++++++++++++++++++++---
 2 files changed, 53 insertions(+), 5 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index cfb2ce6..2e54a7b 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -64,6 +64,7 @@ struct BufferCacheKey {
     size_milli: u32,
     font: Option<String>,
     is_vertical: bool,
+    attrs: crate::scene::paint::TextAttrs,
 }
 
 #[derive(Clone)]
@@ -89,6 +90,18 @@ fn find_cased_family(fs: &FontSystem, name: &str) -> Option<String> {
 }
 
 pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+    get_text_buffer_attrs(fs, text, size, font, crate::scene::paint::TextAttrs::default())
+}
+
+/// [`get_text_buffer`] plus shaping attributes (italic / weight) — the backend's shape entry
+/// for `Prim::Text` prims that carry [`TextAttrs`] (the font picker's style-variant previews).
+pub fn get_text_buffer_attrs(
+    fs: &mut FontSystem,
+    text: &str,
+    size: f32,
+    font: Option<&str>,
+    text_attrs: crate::scene::paint::TextAttrs,
+) -> Buffer {
     let scale = crate::scale::scale_factor();
     let mut font_size = size;
     let mut family_name = None;
@@ -109,6 +122,7 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
         size_milli: size_key,
         font: family_name.clone(),
         is_vertical,
+        attrs: text_attrs,
     };
 
     let cached = BUFFER_CACHE.with(|cache| {
@@ -198,6 +212,12 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
         }
     };
     attrs = attrs.family(family);
+    if text_attrs.italic {
+        attrs = attrs.style(glyphon::Style::Italic);
+    }
+    if let Some(w) = text_attrs.weight {
+        attrs = attrs.weight(glyphon::Weight(w));
+    }
     buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
     buf.shape_until_scroll(fs, true);
 
@@ -1886,14 +1906,14 @@ impl<A: Application> EngineState<A> {
         if self.inner.as_ref().unwrap().display_list_text() {
             let fs = &mut self.wgpu_adapter.as_mut().unwrap().font_system;
             for item in &dl.items {
-                if let crate::scene::paint::Prim::Text { text, x, y, font_size, color, font, bounds } = &item.prim {
+                if let crate::scene::paint::Prim::Text { text, x, y, font_size, color, font, bounds, attrs } = &item.prim {
                     let clip = item.clip.map(|c| [c.x, c.y, c.x + c.width, c.y + c.height]);
                     let merged = match (clip, *bounds) {
                         (Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
                         (Some(a), None) => Some(a),
                         (None, b) => b,
                     };
-                    let buffer = get_text_buffer(fs, text, *font_size, font.as_deref());
+                    let buffer = get_text_buffer_attrs(fs, text, *font_size, font.as_deref(), *attrs);
                     self.dl_text_items.push(TextItem {
                         buffer,
                         x: *x,
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 3d916ef..9da49a8 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -50,8 +50,19 @@ pub enum Prim {
     /// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
     /// for the glyph pass (Phase 6: the backend renders these through glyphon when the app
     /// opts in via `Application::display_list_text`; the paint walk's clip additionally
-    /// applies through the item's `clip`).
-    Text { text: String, x: f32, y: f32, font_size: f32, color: [u8; 3], font: Option<String>, bounds: Option<[f32; 4]> },
+    /// applies through the item's `clip`). `attrs` carries the optional shaping attributes
+    /// beyond family+size (the font picker's italic/weight preview variants).
+    Text { text: String, x: f32, y: f32, font_size: f32, color: [u8; 3], font: Option<String>, bounds: Option<[f32; 4]>, attrs: TextAttrs },
+}
+
+/// Optional shaping attributes for a [`Prim::Text`] — the subset a widget can request beyond
+/// family + size. `weight` is the OpenType weight (400 regular, 700 bold); `None` leaves the
+/// family default. Kept toolkit-plain (no glyphon types) like the rest of the scene layer;
+/// the backend maps them onto `glyphon::Style`/`Weight` at shape time.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
+pub struct TextAttrs {
+    pub italic: bool,
+    pub weight: Option<u16>,
 }
 
 /// A primitive plus the scissor rect it must be clipped to (`None` = unclipped).
@@ -210,10 +221,27 @@ impl PaintCtx {
         color: [u8; 3],
         font: Option<String>,
         bounds: Option<[f32; 4]>,
+    ) {
+        self.text_attrs(text, x, y, font_size, color, font, bounds, TextAttrs::default());
+    }
+
+    /// [`text_with`](PaintCtx::text_with) plus shaping attributes (italic / weight) — what the
+    /// font picker's style-variant previews need beyond family + size.
+    #[allow(clippy::too_many_arguments)]
+    pub fn text_attrs(
+        &mut self,
+        text: impl Into<String>,
+        x: f32,
+        y: f32,
+        font_size: f32,
+        color: [u8; 3],
+        font: Option<String>,
+        bounds: Option<[f32; 4]>,
+        attrs: TextAttrs,
     ) {
         let (ox, oy) = self.offset;
         let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
-        self.push(Prim::Text { text: text.into(), x: x + ox, y: y + oy, font_size, color, font, bounds });
+        self.push(Prim::Text { text: text.into(), x: x + ox, y: y + oy, font_size, color, font, bounds, attrs });
     }
 
     /// Consume the context and return the accumulated display list.