GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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.