1use draw::{GlyphRun, Glyphs, RenderError};
2use parley::{
3 Alignment, AlignmentOptions, Cluster, FontContext, FontFamily, FontFamilyName, GenericFamily,
4 Layout, LayoutContext, OverflowWrap, PositionedLayoutItem, StyleProperty, fontique::Blob,
5};
6use std::{borrow::Cow, collections::HashMap, rc::Rc};
7
8/// Shaped interface text, in logical pixels: a single line unless fitted to a width.
9pub(crate) struct Label {
10 pub layout: Layout<()>,
11 pub size: [f32; 2],
12 pub key: LabelKey,
13}
14
15type LabelKey = (String, u32, bool, bool, Option<String>);
16
17/// Labels shaped this frame or the previous one, keyed by their text and size.
18#[derive(Default)]
19pub(crate) struct Texts {
20 fonts: FontContext,
21 context: LayoutContext<()>,
22 /// By text, size, weight and preferred family, with the frame each was last used.
23 cache: HashMap<LabelKey, (Rc<Label>, u64)>,
24 /// The Last Resort font, once looked for.
25 last_resort: Option<Option<Blob<u8>>>,
26}
27
28impl Texts {
29 /// `text` in the interface's regular face, `size` logical pixels.
30 pub fn label(&mut self, text: &str, size: f32, frame: u64) -> Rc<Label> {
31 self.styled(text, size, false, false, None, frame)
32 }
33
34 /// `font` names a family to prefer over the interface's.
35 pub fn styled(
36 &mut self,
37 text: &str,
38 size: f32,
39 bold: bool,
40 italic: bool,
41 font: Option<&str>,
42 frame: u64,
43 ) -> Rc<Label> {
44 let key = (
45 text.to_owned(),
46 size.to_bits(),
47 bold,
48 italic,
49 font.map(str::to_owned),
50 );
51 if let Some((label, touched)) = self.cache.get_mut(&key) {
52 *touched = frame;
53 return label.clone();
54 }
55 let mut builder = self
56 .context
57 .ranged_builder(&mut self.fonts, text, 1.0, false);
58 // Fontconfig configurations without a system-ui alias still have a sans-serif.
59 let system =
60 [GenericFamily::SystemUi, GenericFamily::SansSerif].map(FontFamilyName::Generic);
61 let families: Vec<_> = font
62 .into_iter()
63 .map(FontFamilyName::named)
64 .chain(system)
65 .collect();
66 builder.push_default(StyleProperty::FontFamily(FontFamily::List(Cow::Owned(
67 families,
68 ))));
69 builder.push_default(StyleProperty::FontSize(size));
70 builder.push_default(StyleProperty::OverflowWrap(OverflowWrap::Anywhere));
71 if bold {
72 builder.push_default(StyleProperty::FontWeight(parley::FontWeight::SEMI_BOLD));
73 }
74 if italic {
75 builder.push_default(StyleProperty::FontStyle(parley::FontStyle::Italic));
76 }
77 let mut layout = builder.build(text);
78 layout.break_all_lines(None);
79 let size = [layout.full_width(), layout.height()];
80 let label = Rc::new(Label {
81 layout,
82 size,
83 key: key.clone(),
84 });
85 self.cache.insert(key, (label.clone(), frame));
86 label
87 }
88
89 /// `label` cut short to fit `width` with an ellipsis.
90 pub fn ellipsis(&mut self, label: &Label, width: f32, frame: u64) -> Rc<Label> {
91 let (text, size, bold, italic, font) = &label.key;
92 let (size, font) = (f32::from_bits(*size), font.as_deref());
93 let room = width - self.styled("…", size, *bold, *italic, font, frame).size[0];
94 let mut end = 0;
95 let mut advance = 0.0;
96 let mut cluster = Cluster::from_byte_index(&label.layout, 0);
97 while let Some(next) = cluster {
98 advance += next.advance();
99 if advance > room {
100 break;
101 }
102 end = next.text_range().end;
103 cluster = next.next_logical();
104 }
105 let cut = format!("{}…", text[..end].trim_end());
106 self.styled(&cut, size, *bold, *italic, font, frame)
107 }
108
109 /// Whether the interface's fonts or their fallbacks draw `text`: some glyph, and none the
110 /// missing glyph or a placeholder of macOS's Last Resort font.
111 pub fn shows(&mut self, text: &str, frame: u64) -> bool {
112 let last_resort = self
113 .last_resort
114 .get_or_insert_with(|| {
115 let family = self.fonts.collection.family_by_name(".LastResort")?;
116 family
117 .fonts()
118 .first()?
119 .load(Some(&mut self.fonts.source_cache))
120 })
121 .clone();
122 let label = self.styled(text, 16.0, false, false, None, frame);
123 let mut glyphs = 0;
124 let drawn = label.layout.lines().all(|line| {
125 line.items().all(|item| match item {
126 PositionedLayoutItem::GlyphRun(run) => {
127 glyphs += run.glyphs().count();
128 // By contents: the source cache may load the font afresh.
129 let data = run.run().font().font.data.data();
130 last_resort.as_ref().is_none_or(|font| font.data() != data)
131 && run.glyphs().all(|glyph| glyph.id != 0)
132 }
133 PositionedLayoutItem::InlineBox(_) => true,
134 })
135 });
136 drawn && glyphs > 0
137 }
138
139 /// Makes `family` the interface's font, where fontique doesn't know the system's.
140 pub fn set_system_font(&mut self, family: &str) {
141 let collection = &mut self.fonts.collection;
142 if let Some(id) = collection.family_id(family) {
143 collection.set_generic_families(GenericFamily::SystemUi, std::iter::once(id));
144 self.cache.clear();
145 }
146 }
147
148 pub fn fall_back_to(&mut self, families: &[String]) {
149 draw::fall_back_to(&mut self.fonts.collection, families);
150 self.cache.clear();
151 }
152
153 pub fn preview_font(&mut self, data: Blob<u8>, family: &str) {
154 self.fonts.collection.register_fonts(
155 data,
156 Some(parley::fontique::FontInfoOverride {
157 family_name: Some(family),
158 ..Default::default()
159 }),
160 );
161 self.cache.clear();
162 }
163
164 pub fn prune(&mut self, frame: u64) {
165 self.cache.retain(|_, (_, touched)| *touched + 1 >= frame);
166 }
167}
168
169impl Label {
170 /// The text broken into lines no wider than `width` and aligned across it.
171 pub fn wrapped(&self, width: f32, center: bool) -> Rc<Label> {
172 let mut layout = self.layout.clone();
173 layout.break_all_lines(Some(width));
174 let alignment = if center {
175 Alignment::Center
176 } else {
177 Alignment::Start
178 };
179 layout.align(alignment, AlignmentOptions::default());
180 Rc::new(Label {
181 size: [width, layout.height()],
182 layout,
183 key: self.key.clone(),
184 })
185 }
186}
187
188impl Glyphs for Label {
189 fn runs(
190 &self,
191 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
192 ) -> Result<(), RenderError> {
193 for line in self.layout.lines() {
194 let metrics = line.metrics();
195 for item in line.items() {
196 if let PositionedLayoutItem::GlyphRun(run) = item {
197 draw::paint_parley_run(
198 &run,
199 "",
200 metrics.baseline,
201 0.0,
202 [metrics.block_min_coord, metrics.line_height],
203 |_| None,
204 None,
205 paint,
206 )?;
207 }
208 }
209 }
210 Ok(())
211 }
212}