Add retained paints and shared text selection

This commit is contained in:
iris committed 2026-09-10 18:35:24 -04:00
1 parent 1e6d3b1edd
commit a33fbca966
42 files changed
+2424 -470

No files matched your search

+489 -142
View File
@@ -1,171 +1,518 @@
use std::marker::Destruct;
use crate::util::{Dirty, RefCounter};
use std::{
cell::RefCell,
fmt,
rc::Rc,
sync::mpsc::{Receiver, Sender, channel},
};
/// Encoded, straight-alpha sRGB at an input boundary.
///
/// Palette literals, decoded images and colour glyph bitmaps use this
/// convention. A solid paint is converted to linear light when it enters the
/// paint table; the renderer never performs colour arithmetic on these bytes.
#[repr(C)]
#[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
pub struct Color<T> {
pub r: T,
pub g: T,
pub b: T,
pub a: T,
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Srgba8 {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
/// Required by parley's `Brush`, which every text style is generic over. Opaque
/// black rather than transparent: a brush that was never set should be visible
/// and obviously unstyled, not invisible.
impl<T: ColorNum> Default for Color<T> {
impl Srgba8 {
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const GRAY: Self = Self::rgb(127, 127, 127);
pub const RED: Self = Self::rgb(255, 0, 0);
pub const ORANGE: Self = Self::rgb(255, 127, 0);
pub const YELLOW: Self = Self::rgb(255, 255, 0);
pub const LIME: Self = Self::rgb(127, 255, 0);
pub const GREEN: Self = Self::rgb(0, 255, 0);
pub const TURQUOISE: Self = Self::rgb(0, 255, 127);
pub const CYAN: Self = Self::rgb(0, 255, 255);
pub const SKY: Self = Self::rgb(0, 127, 255);
pub const BLUE: Self = Self::rgb(0, 0, 255);
pub const PURPLE: Self = Self::rgb(127, 0, 255);
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
pub const NONE: Self = Self::new(0, 0, 0, 0);
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::new(r, g, b, 255)
}
pub fn to_linear(self) -> LinearRgba {
LinearRgba::new(
srgb_to_linear(self.r as f32 / 255.0),
srgb_to_linear(self.g as f32 / 255.0),
srgb_to_linear(self.b as f32 / 255.0),
self.a as f32 / 255.0,
)
}
}
/// Straight-alpha RGBA in linear-light sRGB primaries.
///
/// This is Iris's working representation: manipulate and interpolate colours
/// here, then put the result in [`Paints`]. The GPU paint buffer stores this
/// exact layout as `vec4<f32>`.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LinearRgba {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl LinearRgba {
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
pub const NONE: Self = Self::new(0.0, 0.0, 0.0, 0.0);
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self::new(r, g, b, 1.0)
}
pub fn mul_rgb(self, amount: f32) -> Self {
Self::new(self.r * amount, self.g * amount, self.b * amount, self.a)
}
pub fn darker(self, amount: f32) -> Self {
self.mul_rgb(1.0 - amount)
}
pub fn brighter(self, amount: f32) -> Self {
Self::new(
self.r + (1.0 - self.r) * amount,
self.g + (1.0 - self.g) * amount,
self.b + (1.0 - self.b) * amount,
self.a,
)
}
pub fn to_wgpu(self) -> wgpu::Color {
wgpu::Color {
r: self.r as f64,
g: self.g as f64,
b: self.b as f64,
a: self.a as f64,
}
}
}
fn srgb_to_linear(value: f32) -> f32 {
if value <= 0.04045 {
value / 12.92
} else {
((value + 0.055) / 1.055).powf(2.4)
}
}
/// A description that can be registered in Iris's paint table.
///
/// Only solid paints exist today. Keeping registration behind this trait and
/// making primitives carry [`PaintId`] leaves one place to add gradient or
/// texture paint records later.
pub trait Paint: private::Sealed + 'static {
#[doc(hidden)]
fn add_to(&self, paints: &mut Paints) -> PaintId;
#[doc(hidden)]
fn replace(&self, paints: &mut Paints, slot: u32);
/// Erases this definition so a widget can register it lazily on its
/// first draw. [`PaintId`] overrides this to stay a direct handle.
#[doc(hidden)]
fn into_value(self) -> PaintValue
where
Self: Sized,
{
PaintValue::pending(self)
}
}
impl Paint for Srgba8 {
fn add_to(&self, paints: &mut Paints) -> PaintId {
paints.add_linear(self.to_linear())
}
fn replace(&self, paints: &mut Paints, slot: u32) {
paints.replace_linear(slot, self.to_linear());
}
}
impl Paint for LinearRgba {
fn add_to(&self, paints: &mut Paints) -> PaintId {
paints.add_linear(*self)
}
fn replace(&self, paints: &mut Paints, slot: u32) {
paints.replace_linear(slot, *self);
}
}
mod private {
pub trait Sealed {}
impl Sealed for super::Srgba8 {}
impl Sealed for super::LinearRgba {}
impl Sealed for super::PaintId {}
}
#[derive(Debug)]
struct PaintLease {
slot: u32,
counter: RefCounter,
send: Sender<u32>,
}
impl Clone for PaintLease {
fn clone(&self) -> Self {
Self {
slot: self.slot,
counter: self.counter.clone(),
send: self.send.clone(),
}
}
}
impl Drop for PaintLease {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.slot);
}
}
}
/// A stable reference to one entry in a UI's paint table.
///
/// Built-in IDs name the same reserved entries in every [`Paints`]. IDs
/// returned by [`Paints::add`] retain their slot until the last clone held by a
/// widget, shaped text or retained draw is dropped.
#[derive(Clone, Debug)]
pub struct PaintId {
slot: u32,
lease: Option<PaintLease>,
}
impl PaintId {
pub const BLACK: Self = Self::builtin(0);
pub const WHITE: Self = Self::builtin(1);
pub const GRAY: Self = Self::builtin(2);
pub const RED: Self = Self::builtin(3);
pub const ORANGE: Self = Self::builtin(4);
pub const YELLOW: Self = Self::builtin(5);
pub const LIME: Self = Self::builtin(6);
pub const GREEN: Self = Self::builtin(7);
pub const TURQUOISE: Self = Self::builtin(8);
pub const CYAN: Self = Self::builtin(9);
pub const SKY: Self = Self::builtin(10);
pub const BLUE: Self = Self::builtin(11);
pub const PURPLE: Self = Self::builtin(12);
pub const MAGENTA: Self = Self::builtin(13);
pub const NONE: Self = Self::builtin(14);
const fn builtin(slot: u32) -> Self {
Self { slot, lease: None }
}
pub(crate) fn slot(&self) -> u32 {
self.slot
}
fn is_managed(&self) -> bool {
self.lease.is_some()
}
}
impl Default for PaintId {
fn default() -> Self {
Self::BLACK
}
}
impl<T: ColorNum> Color<T> {
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
pub const GRAY: Self = Self::rgb(T::MID, T::MID, T::MID);
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
pub const TURQUOISE: Self = Self::rgb(T::MIN, T::MAX, T::MID);
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
pub const SKY: Self = Self::rgb(T::MIN, T::MID, T::MAX);
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
pub const PURPLE: Self = Self::rgb(T::MID, T::MIN, T::MAX);
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
pub const NONE: Self = Self::new(T::MIN, T::MIN, T::MIN, T::MIN);
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: T, g: T, b: T) -> Self {
Self { r, g, b, a: T::MAX }
}
pub fn alpha(mut self, a: T) -> Self {
self.a = a;
self
}
pub fn as_arr(self) -> [T; 4] {
[self.r, self.g, self.b, self.a]
impl PartialEq for PaintId {
fn eq(&self, other: &Self) -> bool {
self.slot == other.slot
}
}
pub const trait F32Conversion {
fn to(self) -> f32;
fn from(x: f32) -> Self;
impl Eq for PaintId {}
impl Paint for PaintId {
fn add_to(&self, _paints: &mut Paints) -> PaintId {
self.clone()
}
fn replace(&self, paints: &mut Paints, slot: u32) {
let value = paints.entries[self.slot as usize];
paints.replace_linear(slot, value);
}
fn into_value(self) -> PaintValue {
PaintValue(PaintValueInner::Id(self))
}
}
pub trait ColorNum {
const MIN: Self;
const MID: Self;
const MAX: Self;
struct PendingPaint {
definition: Box<dyn Paint>,
resolved: RefCell<Option<PaintId>>,
}
macro_rules! map_rgb {
($x:ident,$self:ident, $e:tt) => {
#[allow(unused_braces)]
Self {
r: {
let $x = $self.r;
$e
},
g: {
let $x = $self.g;
$e
},
b: {
let $x = $self.b;
$e
},
a: $self.a,
#[derive(Clone)]
enum PaintValueInner {
Id(PaintId),
Pending(Rc<PendingPaint>),
}
/// A widget property containing either an existing paint-table ID or a paint
/// definition that will receive an ID the first time it is drawn.
///
/// Pending definitions are shared across clones and registered only once.
/// Each property replaces its own pending variant with the resulting direct
/// ID after that first resolution, so later draws take the direct path.
#[derive(Clone)]
pub struct PaintValue(PaintValueInner);
impl PaintValue {
fn pending(paint: impl Paint) -> Self {
Self(PaintValueInner::Pending(Rc::new(PendingPaint {
definition: Box::new(paint),
resolved: RefCell::new(None),
})))
}
pub fn resolve(&mut self, paints: &mut Paints) -> &PaintId {
if let PaintValueInner::Pending(pending) = &self.0 {
let resolved = pending.resolved.borrow().clone();
let id = match resolved {
Some(id) => id,
None => {
let id = pending.definition.add_to(paints);
*pending.resolved.borrow_mut() = Some(id.clone());
id
}
};
self.0 = PaintValueInner::Id(id);
}
};
}
impl<T: ColorNum + const F32Conversion> Color<T>
where
Self: const Destruct,
{
pub const fn mul_rgb(self, amt: impl const F32Conversion) -> Self {
let amt = amt.to();
map_rgb!(x, self, { T::from(x.to() * amt) })
let PaintValueInner::Id(id) = &self.0 else {
unreachable!()
};
id
}
pub const fn add_rgb(self, amt: impl const F32Conversion) -> Self {
let amt = amt.to();
map_rgb!(x, self, { T::from(x.to() + amt) })
}
pub const fn darker(self, amt: f32) -> Self {
self.mul_rgb(1.0 - amt)
}
pub const fn brighter(self, amt: f32) -> Self {
map_rgb!(x, self, {
let x = x.to();
T::from(x + (1.0 - x) * amt)
})
}
pub fn map_rgb(self, f: impl Fn(T) -> T) -> Self {
Self {
r: f(self.r),
g: f(self.g),
b: f(self.b),
a: self.a,
}
}
pub fn srgb(r: T, g: T, b: T) -> Self {
Self {
r: s_to_l(r),
g: s_to_l(g),
b: s_to_l(b),
a: T::MAX,
pub fn is(&self, id: &PaintId) -> bool {
match &self.0 {
PaintValueInner::Id(current) => current == id,
PaintValueInner::Pending(pending) => pending.resolved.borrow().as_ref() == Some(id),
}
}
}
fn s_to_l<T: F32Conversion>(x: T) -> T {
let x = x.to();
T::from(if x <= 0.0405 {
x / 12.92
} else {
((x + 0.055) / 1.055).powf(2.4)
})
}
impl ColorNum for u8 {
const MIN: Self = u8::MIN;
const MID: Self = u8::MAX / 2;
const MAX: Self = u8::MAX;
}
impl ColorNum for f32 {
const MIN: Self = 0.0;
const MID: Self = 0.5;
const MAX: Self = 1.0;
}
unsafe impl bytemuck::Pod for Color<u8> {}
const impl F32Conversion for f32 {
fn to(self) -> f32 {
self
}
fn from(x: f32) -> Self {
x
impl fmt::Debug for PaintValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
PaintValueInner::Id(id) => f.debug_tuple("PaintValue").field(id).finish(),
PaintValueInner::Pending(_) => f.write_str("PaintValue(Pending)"),
}
}
}
const impl F32Conversion for u8 {
fn to(self) -> f32 {
self as f32 / 255.0
const BUILTIN_PAINTS: [Srgba8; 15] = [
Srgba8::BLACK,
Srgba8::WHITE,
Srgba8::GRAY,
Srgba8::RED,
Srgba8::ORANGE,
Srgba8::YELLOW,
Srgba8::LIME,
Srgba8::GREEN,
Srgba8::TURQUOISE,
Srgba8::CYAN,
Srgba8::SKY,
Srgba8::BLUE,
Srgba8::PURPLE,
Srgba8::MAGENTA,
Srgba8::NONE,
];
/// CPU-side paint table and the dirty set for its GPU mirror.
pub struct Paints {
entries: Vec<LinearRgba>,
free: Vec<u32>,
dirty: Dirty,
send: Sender<u32>,
recv: Receiver<u32>,
}
impl Paints {
pub fn new() -> Self {
let (send, recv) = channel();
Self {
entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(),
free: Vec::new(),
dirty: Dirty::new_all(),
send,
recv,
}
}
fn from(x: f32) -> Self {
(x * 255.0).clamp(0.0, 255.0) as Self
pub fn add(&mut self, paint: impl Paint) -> PaintId {
paint.add_to(self)
}
fn add_linear(&mut self, value: LinearRgba) -> PaintId {
self.free_released();
let slot = if let Some(slot) = self.free.pop() {
self.entries[slot as usize] = value;
self.dirty.mark(slot as usize);
slot
} else {
let slot = self.entries.len() as u32;
self.entries.push(value);
self.dirty.mark(slot as usize);
slot
};
PaintId {
slot,
lease: Some(PaintLease {
slot,
counter: RefCounter::new(),
send: self.send.clone(),
}),
}
}
/// Replaces one managed paint in place. Every primitive keeps the same
/// index, so a theme change dirties this table and no primitive buffer.
pub fn set(&mut self, id: &PaintId, paint: impl Paint) {
assert!(
id.is_managed(),
"a reserved built-in paint cannot be replaced; allocate a theme slot with Paints::add"
);
paint.replace(self, id.slot);
}
fn replace_linear(&mut self, slot: u32, value: LinearRgba) {
self.entries[slot as usize] = value;
self.dirty.mark(slot as usize);
}
pub fn get(&self, id: &PaintId) -> LinearRgba {
self.entries[id.slot as usize]
}
pub fn free_released(&mut self) {
for slot in self.recv.try_iter() {
self.entries[slot as usize] = LinearRgba::NONE;
self.dirty.mark(slot as usize);
self.free.push(slot);
}
}
/// A new GPU device has no copy of this table even when the CPU-side UI
/// and its paint IDs survived an Android surface recreation.
pub fn reupload(&mut self) {
self.dirty.mark_all();
}
pub fn for_upload(&mut self) -> (&[LinearRgba], &mut Dirty) {
(&self.entries, &mut self.dirty)
}
}
impl Default for Paints {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn srgb_bytes_become_linear_without_transforming_alpha() {
let got = Srgba8::new(17, 127, 255, 64).to_linear();
assert!((got.r - 0.005605).abs() < 0.000001);
assert!((got.g - 0.212231).abs() < 0.000001);
assert_eq!(got.b, 1.0);
assert!((got.a - 64.0 / 255.0).abs() < f32::EPSILON);
}
#[test]
fn changing_a_paint_keeps_its_id_and_dirties_only_its_slot() {
let mut paints = Paints::new();
let id = paints.add(Srgba8::rgb(17, 17, 27));
let slot = id.slot();
let (_, dirty) = paints.for_upload();
dirty.clear();
paints.set(&id, Srgba8::rgb(205, 214, 244));
let (_, dirty) = paints.for_upload();
assert!(dirty.contains(slot as usize));
assert_eq!(id.slot(), slot);
}
#[test]
fn a_released_paint_slot_is_reused_only_after_the_last_clone() {
let mut paints = Paints::new();
let id = paints.add(Srgba8::RED);
let slot = id.slot();
let held = id.clone();
drop(id);
paints.free_released();
let other = paints.add(Srgba8::GREEN);
assert_ne!(other.slot(), slot);
drop(held);
paints.free_released();
let reused = paints.add(Srgba8::BLUE);
assert_eq!(reused.slot(), slot);
}
#[test]
fn cloned_pending_paints_register_once_and_then_become_direct_ids() {
let mut paints = Paints::new();
let mut first = Srgba8::rgb(17, 17, 27).into_value();
let mut second = first.clone();
let before = paints.entries.len();
let first_id = first.resolve(&mut paints).clone();
let second_id = second.resolve(&mut paints).clone();
assert_eq!(first_id, second_id);
assert_eq!(paints.entries.len(), before + 1);
assert!(first.is(&first_id));
assert!(second.is(&first_id));
}
#[test]
fn independently_constructed_inline_solids_get_independent_slots() {
let mut paints = Paints::new();
let mut first = Srgba8::rgb(23, 42, 71).into_value();
let mut second = Srgba8::rgb(23, 42, 71).into_value();
let before = paints.entries.len();
let first_id = first.resolve(&mut paints).clone();
let second_id = second.resolve(&mut paints).clone();
assert_ne!(first_id, second_id);
assert_eq!(paints.entries.len(), before + 2);
}
#[test]
fn explicit_theme_paints_with_equal_values_remain_independent() {
let mut paints = Paints::new();
let first = paints.add(Srgba8::rgb(23, 42, 71));
let second = paints.add(Srgba8::rgb(23, 42, 71));
assert_ne!(first.slot(), second.slot());
}
}
+29 -18
View File
@@ -1,4 +1,4 @@
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
@@ -32,7 +32,7 @@ pub struct FontDiagnostics {
pub struct TextData {
pub font_cx: FontContext,
pub layout_cx: LayoutContext<UiColor>,
pub layout_cx: LayoutContext<PaintId>,
scale_cx: ScaleContext,
pub atlas: GlyphAtlas,
/// Physical pixels per dp -- a second copy of
@@ -240,8 +240,13 @@ impl TextData {
}
}
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
pub fn place(
&mut self,
buffer: &TextBuffer,
textures: &mut Textures,
) -> (Vec<PlacedGlyph>, Vec<PaintId>) {
let mut placed = Vec::new();
let mut paints = Vec::new();
for line in buffer.layout.lines() {
for item in line.items() {
let PositionedLayoutItem::GlyphRun(run) = item else {
@@ -250,7 +255,10 @@ impl TextData {
let font = run.run().font();
let font_size = run.run().font_size();
let coords = run.run().normalized_coords();
let run_color = run.style().brush;
let run_color = run.style().brush.clone();
if !paints.contains(&run_color) {
paints.push(run_color.clone());
}
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
else {
continue;
@@ -301,12 +309,12 @@ impl TextData {
glyph.x.floor() + entry.left as f32,
glyph.y.floor() - entry.top as f32,
),
color: run_color,
paint: run_color.slot(),
});
}
}
}
placed
(placed, paints)
}
pub fn render(
@@ -318,11 +326,12 @@ impl TextData {
density: f32,
) -> RenderedText {
buffer.shape(self, attrs, width, density);
let glyphs = self.place(buffer, textures);
let (glyphs, paints) = self.place(buffer, textures);
RenderedText {
glyphs: std::sync::Arc::new(glyphs),
paints: std::sync::Arc::new(paints),
size: buffer.size(),
color: attrs.color,
color: attrs.color.clone(),
generation: self.atlas.generation(),
}
}
@@ -359,7 +368,7 @@ impl Family {
#[derive(Clone, PartialEq)]
pub struct SpanStyle {
pub range: Range<usize>,
pub color: Option<UiColor>,
pub color: Option<PaintId>,
pub family: Option<Family>,
pub font_size: Option<f32>,
pub bold: bool,
@@ -379,7 +388,7 @@ impl SpanStyle {
underline: false,
}
}
pub fn color(mut self, color: UiColor) -> Self {
pub fn color(mut self, color: PaintId) -> Self {
self.color = Some(color);
self
}
@@ -407,7 +416,7 @@ impl SpanStyle {
#[derive(Clone, PartialEq)]
pub struct TextAttrs {
pub color: UiColor,
pub color: PaintId,
pub font_size: f32,
pub line_height: f32,
pub family: Family,
@@ -421,7 +430,7 @@ impl Default for TextAttrs {
fn default() -> Self {
let size = 16.0;
Self {
color: UiColor::WHITE,
color: PaintId::WHITE,
font_size: size,
line_height: size * LINE_HEIGHT_MULT,
family: Family::SansSerif,
@@ -436,7 +445,7 @@ impl Default for TextAttrs {
/// them apart is how they get out of step.
pub struct TextBuffer {
text: String,
layout: Layout<UiColor>,
layout: Layout<PaintId>,
spans: Vec<SpanStyle>,
shaped: Option<(TextAttrs, Option<f32>, f32)>,
}
@@ -464,7 +473,7 @@ impl TextBuffer {
&self.text
}
pub fn layout(&self) -> &Layout<UiColor> {
pub fn layout(&self) -> &Layout<PaintId> {
&self.layout
}
@@ -515,11 +524,11 @@ impl TextBuffer {
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
attrs.line_height * density,
)));
builder.push_default(StyleProperty::Brush(attrs.color));
builder.push_default(StyleProperty::Brush(attrs.color.clone()));
for (span, family) in self.spans.iter().zip(&span_families) {
let range = span.range.clone();
if let Some(color) = span.color {
builder.push(StyleProperty::Brush(color), range.clone());
if let Some(color) = &span.color {
builder.push(StyleProperty::Brush(color.clone()), range.clone());
}
if let Some(family) = family {
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
@@ -563,8 +572,10 @@ fn hash_coords(coords: &[i16]) -> u64 {
#[derive(Clone)]
pub struct RenderedText {
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
/// The unique handles whose compact slots the glyphs above carry.
pub paints: std::sync::Arc<Vec<PaintId>>,
pub size: Vec2,
pub color: UiColor,
pub color: PaintId,
/// The [`GlyphAtlas::generation`] the glyphs above were placed against.
/// A holder must re-render rather than re-emit these quads once the
/// atlas has moved on (`GlyphAtlas::clear`'s doc says what happens