Skip to main content

ui/
fontpick.rs

1// fontpick -- the system's fonts in the UI: the font database and its rules, loading a
2// choice, text in such a font, a font picker and a font dialog
3//
4//   FontSel { family, weight, italic, size, axes }   a choice: a family (also a generic one:
5//                                    "sans-serif", "serif", "monospace" ...), 100..900, italic,
6//                                    the size in points, axis values of a variable family
7//                                    (FontSel::new: none; axis / set_axis)
8//   ui.fonts() -> &FontDb            the installed fonts, scanned on first use (cache
9//                                    cache://fontdb.fdb; jfgui_FONTDIRS: these folders, no cache)
10//   ui.font_conf() -> &FontConf      generic families, substitutes, the languages' fonts
11//                                    (text::fontconf: a light fontconfig)
12//   ui.font_load(&sel) -> Option<u32>   resolve (FontDb::resolve in the UI language) and load
13//                                    into ui.text: a font id for set_font / cmd_text; each
14//                                    family / weight / italic once.  A variable face: its
15//                                    instance (FontDb::load_var): weight -> wght, italic -> ital
16//                                    / slnt, the size -> opsz, the axes named win; the instances
17//                                    of the VAR_KEEP most recent such choices and the app
18//                                    theme's faces stay loaded (older ones are unloaded, their
19//                                    ids may be reused)
20//   ui.font_resolved(&sel) -> String the installed family font_load takes ("" none)
21//   ui.font_register(family, weight, italic, font)   a font of the app's own (load_mem) under
22//                                    a family name for font_load; font_registered() lists them
23//   ui.text_in_font(font, size_pt, s)   one line of text in a font id at a size in points
24//   ui.font_picker(id, &mut sel) -> EV  a drop-down button with the family (a search box and
25//                                    the families open below it: typing filters and takes
26//                                    the first match, Up / Down / a click choose) and a size
27//                                    box in points; EV_ACT when sel changed
28//   ui.font_dialog(id, &open, &mut sel) -> EV   a modal dialog window "Schriftart": search,
29//                                    families, the family's styles, sizes, a preview in the
30//                                    font, OK / Abbrechen; works on a copy of sel and writes it
31//                                    back on OK (EV_ACT); closing clears open.  A variable
32//                                    family lists its named instances as styles and gets a
33//                                    slider and a number box per axis (fvar's range and name;
34//                                    hidden axes not), opsz with "Automatisch" (follows the size)
35//   font_language() -> String        the language the fonts follow: the user's tag of the UI
36//                                    language (ja-JP, zh-Hant-TW), else its code
37//
38// The runtime hands font_language() to the system fallback (text::fallback, again when the
39// UI language changes), so missing Han characters take the forms of the language.  Ids:
40// the scope of the widget's id, then "family", "search", "list", "styles", "size",
41// "sizes", the axes' "axis" scope with per axis id_n(k) (slider), id_n(k + 100) (number
42// box), "opsz_auto" (ids.rs).
43
44use alloc::collections::BTreeMap;
45use alloc::format;
46use alloc::string::String;
47use alloc::vec::Vec;
48
49use text::fontconf::FontConf;
50use text::fontdb::{FontDb, FDFF_ITALIC, FDFF_OBLIQUE, FDFF_VAR};
51use text::VarAxis;
52
53use super::consts::*;
54use super::value::RangeF;
55use super::win::Shared;
56use super::Ui;
57
58const ROW_H: u32 = 28;
59const SEARCH_CAP: u32 = 128;
60const SIZES: [f64; 16] = [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 28.0, 36.0, 48.0, 72.0, 96.0];
61const RANGE_PT: RangeF = RangeF::new(1.0, 1638.0, 1.0, 1, 0);
62const SAMPLE: &str = "AaBbYyZz 0123456789";
63/// the instances of variable choices font_load keeps loaded
64const VAR_KEEP: usize = 64;
65
66/// a font choice (see the module header)
67#[derive(Clone, Debug, PartialEq)]
68pub struct FontSel {
69    pub family: String,
70    pub weight: u32,
71    pub italic: bool,
72    /// points
73    pub size: f64,
74    /// axis values of a variable family in user units by tag (`(*b"wdth", 87.5)`); the axes
75    /// not named follow weight (wght), italic (ital, slnt) and size (opsz), else their
76    /// defaults; families that are not variable ignore them
77    pub axes: Vec<([u8; 4], f32)>,
78}
79
80impl FontSel {
81    /// regular, upright, no axis values
82    pub fn new(family: &str, size: f64) -> FontSel {
83        FontSel { family: String::from(family), weight: 400, italic: false, size, axes: Vec::new() }
84    }
85
86    /// the value `axes` names for a tag (None: the tag follows weight / italic / size or its
87    /// default)
88    pub fn axis(&self, tag: [u8; 4]) -> Option<f32> {
89        self.axes.iter().find(|a| a.0 == tag).map(|a| a.1)
90    }
91
92    /// name an axis value (replacing the tag's); NaN takes the tag out again
93    pub fn set_axis(&mut self, tag: [u8; 4], v: f32) {
94        self.axes.retain(|a| a.0 != tag);
95        if !v.is_nan() {
96            self.axes.push((tag, v));
97        }
98    }
99}
100
101impl Default for FontSel {
102    fn default() -> FontSel {
103        FontSel::new("sans-serif", 11.0)
104    }
105}
106
107// a style of the dialog's list: a face, or a named instance of a variable face (face index,
108// label, weight, italic, the axis values it names)
109struct StyleRow {
110    face: i32,
111    name: String,
112    weight: u32,
113    italic: bool,
114    axes: Vec<([u8; 4], f32)>,
115}
116
117// a variable choice of font_entry: family, weight, italic, size bits, axes (tag, value bits)
118type VarKey = (String, u32, bool, u64, Vec<([u8; 4], u32)>);
119
120// the state of one picker / dialog between frames
121#[derive(Default)]
122struct Pick {
123    open: bool,
124    search: String,
125    filt: Vec<u32>,
126    sel: i32,
127    style: i32,
128    size: i32,
129    work: FontSel,
130    rect: Option<Shared<WinRect>>,
131    fresh: bool, // the dialog was opened: reset its lists inside its window
132}
133
134/// shared state (Ui::m.fonts)
135#[derive(Default)]
136pub(crate) struct S {
137    db: Option<FontDb>,
138    conf: Option<FontConf>,
139    loaded: BTreeMap<(String, u32, bool), (i32, String)>,
140    /// choices on variable faces, oldest first: (key, font id, family)
141    vloaded: Vec<(VarKey, i32, String)>,
142    picks: BTreeMap<u32, Pick>,
143    /// the UI language the fallback follows (i18n::language() + 1; 0: not set yet)
144    lang_n: u32,
145    /// (ROADMAP §6) font_register: the families of the app's own fonts, in order
146    registered: Vec<String>,
147}
148
149/// the language the fonts follow: the user's tag of the UI language, else its code
150pub fn font_language() -> String {
151    let code = super::i18n::language_code();
152    super::i18n::user_languages()
153        .into_iter()
154        .find(|u| u.split(['-', '_']).next().is_some_and(|p| p.eq_ignore_ascii_case(&code)))
155        .unwrap_or(code)
156}
157
158// the values of named axes as a key (f32 bits)
159fn var_key(sel: &FontSel) -> VarKey {
160    let mut axes: Vec<([u8; 4], u32)> = sel.axes.iter().map(|a| (a.0, a.1.to_bits())).collect();
161    axes.sort_unstable();
162    (sel.family.to_lowercase(), sel.weight, sel.italic, sel.size.to_bits(), axes)
163}
164
165// the axis values a style row names: those that the mapping of weight / italic does not give
166// (wght never: the weight; opsz only when the instances differ in it, else it follows the size)
167fn row_axes(axes: &[VarAxis], coords: &[f32], italic: bool, opsz_named: bool) -> Vec<([u8; 4], f32)> {
168    let has_ital = axes.iter().any(|a| &a.tag == b"ital" && a.max >= 1.0);
169    let mut out = Vec::new();
170    for (a, &v) in axes.iter().zip(coords) {
171        let mapped = match &a.tag {
172            b"wght" => continue,
173            b"opsz" if !opsz_named => continue,
174            b"opsz" => f32::NAN,
175            b"ital" if italic && a.max >= 1.0 => 1.0,
176            b"slnt" if italic && !has_ital && a.min < 0.0 => a.min,
177            _ => a.def,
178        };
179        if !(mapped - v).abs().le(&0.001) {
180            out.push((a.tag, v));
181        }
182    }
183    out
184}
185
186// the slider range of an axis: whole steps for wide ranges, tenths / hundredths for narrow
187fn axis_range(a: &VarAxis) -> RangeF {
188    let span = (a.max - a.min) as f64;
189    let (step, dec) = if span >= 50.0 { (1.0, 0) } else if span >= 2.0 { (0.1, 1) } else { (0.01, 2) };
190    RangeF::new(a.min as f64, a.max as f64, step, dec, 0)
191}
192
193fn has_ci(hay: &str, needle: &str) -> bool {
194    needle.is_empty() || hay.to_lowercase().contains(&needle.to_lowercase())
195}
196
197// the label of a style: the face's own name (FN_STYLE), else from weight and slant
198fn style_label(weight: u32, italic: bool) -> &'static str {
199    let tr = super::i18n::tr;
200    match (weight >= 600, italic) {
201        (false, false) => tr("Standard"),
202        (false, true) => tr("Kursiv"),
203        (true, false) => tr("Fett"),
204        (true, true) => tr("Fett Kursiv"),
205    }
206}
207
208impl Ui {
209    fn fonts_db(&mut self) -> &mut FontDb {
210        self.m.fonts.db.get_or_insert_with(|| {
211            let mut db = FontDb::new();
212            let cache = plat::env("jfgui_FONTDIRS").is_none().then_some("cache://fontdb.fdb");
213            if let Some(c) = cache {
214                db.load_cache(c);
215            }
216            #[cfg(not(target_arch = "wasm32"))]
217            db.scan(0);
218            if let Some(c) = cache {
219                db.save_cache(c);
220            }
221            db
222        })
223    }
224
225    /// the installed fonts (scanned on first use)
226    pub fn fonts(&mut self) -> &FontDb {
227        self.fonts_db()
228    }
229
230    /// the rules for generic families, substitutes and languages
231    pub fn font_conf(&mut self) -> &FontConf {
232        self.m.fonts.conf.get_or_insert_with(FontConf::system)
233    }
234
235    // (font_load, font_resolved) the font of a choice: (id, family), loaded once; a variable
236    // face's instance for weight, italic, size and axes (the VAR_KEEP most recent kept)
237    fn font_entry(&mut self, sel: &FontSel) -> (i32, String) {
238        let key = (sel.family.to_lowercase(), sel.weight, sel.italic);
239        if let Some((id, fam)) = self.m.fonts.loaded.get(&key).cloned()
240            && id >= 0
241            && self.text.valid(id as u32)
242        {
243            // a static face, or the app's own font (a variable one varies with the axes)
244            if sel.axes.is_empty() || self.text.var_axes(id as u32).is_empty() {
245                return (id, fam);
246            }
247            let v = self.text.vary(id as u32, &sel.axes);
248            return (if v >= 0 { v } else { id }, fam);
249        }
250        let vk = var_key(sel);
251        if let Some(e) = self.m.fonts.vloaded.iter().find(|e| e.0 == vk)
252            && e.1 >= 0
253            && self.text.valid(e.1 as u32)
254        {
255            return (e.1, e.2.clone());
256        }
257        self.fonts_db();
258        self.font_conf();
259        let lang = font_language();
260        let s = &mut self.m.fonts;
261        let (Some(db), Some(conf)) = (s.db.as_mut(), s.conf.as_ref()) else { return (-1, String::new()) };
262        let Some((fi, syn, fam)) = db.resolve(conf, &sel.family, &lang, sel.weight, sel.italic) else {
263            s.loaded.insert(key, (-1, String::new()));
264            return (-1, String::new());
265        };
266        if db.face(fi).is_none_or(|f| f.flags & FDFF_VAR == 0) {
267            let id = db.load_face(&mut self.text, fi, syn);
268            s.loaded.insert(key, (id, fam.clone()));
269            return (id, fam);
270        }
271        let id = db.load_var(&mut self.text, fi, syn, sel.weight, sel.italic, Some(sel.size as f32), &sel.axes);
272        s.vloaded.retain(|e| e.0 != vk);
273        s.vloaded.push((vk, id, fam.clone()));
274        if s.vloaded.len() > VAR_KEEP {
275            // the oldest choice: its instance goes unless another choice or the app theme's
276            // faces still have it
277            let (_, old, _) = s.vloaded.remove(0);
278            let used = s.vloaded.iter().any(|e| e.1 == old)
279                || s.loaded.values().any(|e| e.0 == old)
280                || self.look.fonts.contains(&(old as u32));
281            if old >= 0 && !used && !self.text.var_coords(old as u32).is_empty() {
282                self.text.unload(old as u32);
283            }
284        }
285        (id, fam)
286    }
287
288    /// the font of a choice loaded into ui.text -> its id (None: no installed font fits)
289    pub fn font_load(&mut self, sel: &FontSel) -> Option<u32> {
290        let (id, _) = self.font_entry(sel);
291        (id >= 0).then_some(id as u32)
292    }
293
294    /// the installed family font_load takes for a choice ("" none)
295    pub fn font_resolved(&mut self, sel: &FontSel) -> String {
296        self.font_entry(sel).1
297    }
298
299    /// a font of the app's own (an id of ui.text.load_mem) under a family name: font_load
300    /// takes it for that family, weight and slant without asking the installed fonts; the
301    /// rich-text toolbar lists the registered families (rich_edit.rs)
302    pub fn font_register(&mut self, family: &str, weight: u32, italic: bool, font: u32) {
303        let s = &mut self.m.fonts;
304        s.loaded.insert((family.to_lowercase(), weight, italic), (font as i32, String::from(family)));
305        if !s.registered.iter().any(|f| f.eq_ignore_ascii_case(family)) {
306            s.registered.push(String::from(family));
307        }
308    }
309
310    /// the families of font_register, in the order of their first registration
311    pub fn font_registered(&self) -> Vec<String> {
312        self.m.fonts.registered.clone()
313    }
314
315    // (rich_edit.rs) a registered face of a family: exactly (weight, italic) -> its id
316    pub(crate) fn font_registered_face(&self, family: &str, weight: u32, italic: bool) -> Option<u32> {
317        let s = &self.m.fonts;
318        if !s.registered.iter().any(|f| f.eq_ignore_ascii_case(family)) {
319            return None;
320        }
321        s.loaded.get(&(family.to_lowercase(), weight, italic)).filter(|e| e.0 >= 0 && self.text.valid(e.0 as u32)).map(|e| e.0 as u32)
322    }
323
324    // (rich_edit.rs) the family is one of font_register
325    pub(crate) fn font_is_registered(&self, family: &str) -> bool {
326        self.m.fonts.registered.iter().any(|f| f.eq_ignore_ascii_case(family))
327    }
328
329    /// one line of text in font id `font` at size_pt points (the body text colour)
330    #[cfg_attr(jfgui_dev, track_caller)]
331    pub fn text_in_font(&mut self, font: u32, size_pt: f64, s: &str) {
332        let _src = self.src_scope();
333        let s = super::basic::cstr(s);
334        let dpi = self.wp.rt.dpi as f64;
335        let px = (size_pt * 4.0 / 3.0 * dpi / 96.0 * 256.0).clamp(256.0, 2048.0 * 256.0) as i32;
336        let prev = self.text.get_font();
337        self.text.set_font(font, px);
338        let w = self.text.width(s);
339        let asc = self.text.ascent();
340        let lh = self.text.line_h().max(asc + self.text.descent());
341        let r = self.place(w, lh);
342        let c = self.text_color(TS_BODY);
343        let y = r[1] + ((r[3] - lh) >> 1) + asc;
344        self.cmd_text(r[0], y, s.as_bytes(), font, px, c, r[2]);
345        self.text.set_font(prev.0, prev.1);
346        self.wp.ctx.last_state = 0;
347        self.sem(K_TEXT, 0, Some(s), 0);
348    }
349
350    /// (frame_begin) the fallback follows the UI language
351    pub(crate) fn fonts_frame(&mut self) {
352        let n = super::i18n::language() + 1;
353        if self.m.fonts.lang_n != n {
354            self.m.fonts.lang_n = n;
355            self.text.set_fallback_lang(&font_language());
356        }
357    }
358
359    // the indices of the families whose name holds the search text
360    fn font_filter(&mut self, search: &str) -> Vec<u32> {
361        let db = self.fonts_db();
362        (0..db.family_count()).filter(|&i| has_ci(db.family_name(i), search)).map(|i| i as u32).collect()
363    }
364
365    // the index of a family by name (-1: not installed)
366    fn font_family_index(&mut self, family: &str) -> i32 {
367        let db = self.fonts_db();
368        (0..db.family_count()).find(|&i| db.family_name(i).eq_ignore_ascii_case(family)).map_or(-1, |i| i as i32)
369    }
370
371    // the search box and the family list of a picker or dialog (p: its state) -> a family
372    // was chosen (p.work.family); close: a click closes the popup
373    fn font_family_list(&mut self, p: &mut Pick, w: i32, h: i32, close: bool) -> bool {
374        let mut changed = false;
375        self.next_size(w, 0);
376        let mut s = core::mem::take(&mut p.search);
377        let ev = self.textbox(self.id("search"), Some(super::i18n::tr("Suchen")), &mut s, SEARCH_CAP);
378        p.search = s;
379        if ev & EV_ACT != 0 {
380            p.filt = self.font_filter(&p.search.clone());
381            let lid = self.id("list");
382            self.scroll_to(lid, -1, 0);
383            p.sel = if p.filt.is_empty() { -1 } else { 0 };
384            if let Some(&f) = p.filt.first() {
385                p.work.family = String::from(self.fonts_db().family_name(f as usize));
386                changed = true;
387            }
388        }
389        let n = p.filt.len() as i32;
390        let names: Vec<String> = {
391            let db = self.fonts_db();
392            p.filt.iter().map(|&f| String::from(db.family_name(f as usize))).collect()
393        };
394        self.next_size(w, h);
395        let lid = self.id("list");
396        let mut sel = p.sel;
397        let (first, end) = self.list_begin(lid, &mut sel, n, ROW_H);
398        let mut clicked = -1;
399        for row in first..end.min(n) {
400            if self.list_item(row, &names[row as usize]) & EV_ACT != 0 {
401                clicked = row;
402            }
403        }
404        let (lev, _) = self.list_end(&mut sel);
405        p.sel = sel;
406        if clicked >= 0 {
407            p.sel = clicked;
408            p.work.family = names[clicked as usize].clone();
409            changed = true;
410            if close {
411                self.popup_close();
412            }
413        } else if lev & EV_ACT != 0 && (0..n).contains(&p.sel) {
414            p.work.family = names[p.sel as usize].clone();
415            changed = true;
416        }
417        changed
418    }
419
420    // a picker / dialog opened: the whole list, the current family selected and in view
421    fn font_list_reset(&mut self, p: &mut Pick) {
422        p.search.clear();
423        p.filt = self.font_filter("");
424        let fi = self.font_family_index(&p.work.family);
425        p.sel = p.filt.iter().position(|&f| f as i32 == fi).map_or(-1, |k| k as i32);
426        let lid = self.id("list");
427        self.scroll_to(lid, -1, ((p.sel - 4).max(0) as u32 * ROW_H) as i32);
428    }
429
430    /// the font picker (see the module header) -> EV_ACT when sel changed
431    #[cfg_attr(jfgui_dev, track_caller)]
432    pub fn font_picker(&mut self, id: u32, sel: &mut FontSel) -> u32 {
433        let _src = self.src_scope();
434        let mut p = self.m.fonts.picks.remove(&id).unwrap_or_default();
435        self.id_push(id);
436        let mut out = 0;
437        self.box_begin(0, BOX_H | BOX_XCENTER, 8);
438        let bid = self.id("family");
439        self.next_size(220, 0);
440        let label = sel.family.clone();
441        let ev = self.dropdown_button(bid, &label);
442        let open = ev & EV_OPEN != 0;
443        if open != p.open {
444            p.open = open;
445            p.work = sel.clone();
446            if open {
447                self.font_list_reset(&mut p);
448                let sid = self.id("search");
449                self.focus(sid);
450            }
451        }
452        if self.flyout_begin(bid, FL_BELOW | FL_MINW) && self.font_family_list(&mut p, 260, 320, true) {
453            if !sel.family.eq_ignore_ascii_case(&p.work.family) {
454                sel.axes.clear(); // the axis values of the old family
455            }
456            sel.family = p.work.family.clone();
457            out |= EV_ACT;
458        }
459        self.flyout_end();
460        let mut size = sel.size;
461        self.next_size(64, 0);
462        if self.number_box_f(self.id("size"), &mut size, &RANGE_PT) & EV_ACT != 0 && size != sel.size {
463            sel.size = size;
464            out |= EV_ACT;
465        }
466        self.box_end();
467        self.id_pop();
468        self.m.fonts.picks.insert(id, p);
469        out
470    }
471
472    /// the font dialog (see the module header) -> EV_ACT when OK took the choice
473    #[cfg_attr(jfgui_dev, track_caller)]
474    pub fn font_dialog(&mut self, id: u32, open: &Shared<i32>, sel: &mut FontSel) -> u32 {
475        let _src = self.src_scope();
476        let mut p = self.m.fonts.picks.remove(&id).unwrap_or_default();
477        let rect = p.rect.get_or_insert_with(|| Shared::new(WinRect::new(0, 0, 600, 0, 0))).clone();
478        let mut out = 0;
479        self.id_push(id);
480        let shown = open.get() != 0;
481        if shown && !p.open {
482            p.work = sel.clone();
483            p.style = -1;
484            p.size = SIZES.iter().position(|&s| s == p.work.size).map_or(-1, |k| k as i32);
485            p.fresh = true;
486        }
487        p.open = shown;
488        if self.window_begin(id, super::i18n::tr("Schriftart"), Some(open), WF_DIALOG | WF_MODAL | WF_AUTOHEIGHT, Some(&rect)) {
489            if p.fresh {
490                p.fresh = false;
491                self.font_list_reset(&mut p);
492                let sid = self.id("search");
493                self.focus(sid);
494            }
495            self.box_begin(0, BOX_V | PAD_12, 10);
496            self.box_begin(0, BOX_H, 12);
497            // families
498            self.next_size(-1, 0);
499            self.box_begin(0, BOX_V, 6);
500            self.text(TS_BODY_STRONG, super::i18n::tr("Schriftart"));
501            let fam0 = p.work.family.clone();
502            if self.font_family_list(&mut p, -1, 220, false) {
503                p.style = -1;
504                if !fam0.eq_ignore_ascii_case(&p.work.family) {
505                    p.work.axes.clear(); // the axis values of the old family
506                }
507            }
508            self.box_end();
509            // the family's styles (the four synthetic ones for a family that is not installed;
510            // a variable face's named instances)
511            self.next_size(150, 0);
512            self.box_begin(0, BOX_V, 6);
513            self.text(TS_BODY_STRONG, super::i18n::tr("Schriftschnitt"));
514            let fi = self.font_family_index(&p.work.family);
515            let styles = self.font_styles(fi);
516            let var = styles.iter().any(|r| r.face >= 0 && !r.axes.is_empty())
517                || self.font_var_face(&p.work).is_some();
518            if var {
519                // any axis value is a choice: a style is selected when it is exactly that
520                p.style = self.font_style_match(&p.work, &styles);
521            } else if p.style < 0 {
522                // the face closest to the choice
523                p.style = styles
524                    .iter()
525                    .enumerate()
526                    .min_by_key(|(_, s)| (s.italic != p.work.italic) as i32 * 10000 + (s.weight as i32 - p.work.weight as i32).abs())
527                    .map_or(-1, |(k, _)| k as i32);
528                // the choice snaps to that face (what the list shows is what OK takes)
529                if let Some(st) = styles.get(p.style.max(0) as usize).filter(|_| p.style >= 0) {
530                    p.work.weight = st.weight;
531                    p.work.italic = st.italic;
532                }
533            }
534            let sid = self.id("styles");
535            let mut ssel = p.style;
536            let n = styles.len() as i32;
537            self.next_size(-1, 258);
538            let (first, end) = self.list_begin(sid, &mut ssel, n, ROW_H);
539            for row in first..end.min(n) {
540                self.list_item(row, &styles[row as usize].name);
541            }
542            self.list_end(&mut ssel);
543            if ssel != p.style && (0..n).contains(&ssel) {
544                p.style = ssel;
545                let st = &styles[ssel as usize];
546                p.work.weight = st.weight;
547                p.work.italic = st.italic;
548                p.work.axes = st.axes.clone();
549            }
550            self.box_end();
551            // sizes
552            self.next_size(90, 0);
553            self.box_begin(0, BOX_V, 6);
554            self.text(TS_BODY_STRONG, super::i18n::tr("Größe"));
555            let mut size = p.work.size;
556            self.next_size(-1, 0);
557            if self.number_box_f(self.id("size"), &mut size, &RANGE_PT) & EV_ACT != 0 {
558                p.work.size = size;
559                p.size = SIZES.iter().position(|&s| s == size).map_or(-1, |k| k as i32);
560            }
561            let zid = self.id("sizes");
562            let mut zsel = p.size;
563            self.next_size(-1, 220);
564            let (first, end) = self.list_begin(zid, &mut zsel, SIZES.len() as i32, ROW_H);
565            for row in first..end.min(SIZES.len() as i32) {
566                self.list_item(row, &format!("{}", SIZES[row as usize]));
567            }
568            self.list_end(&mut zsel);
569            if zsel != p.size && (0..SIZES.len() as i32).contains(&zsel) {
570                p.size = zsel;
571                p.work.size = SIZES[zsel as usize];
572            }
573            self.box_end();
574            self.box_end();
575            // a variable family's axes
576            if var {
577                self.font_axes(&mut p.work);
578            }
579            // preview
580            self.text(TS_BODY_STRONG, super::i18n::tr("Vorschau"));
581            self.next_size(-1, 96);
582            self.box_begin(0, BOX_V | BOX_CARD | BOX_CLIP | BOX_CENTER | pad(12), 4);
583            let work = p.work.clone();
584            match self.font_load(&work) {
585                Some(f) => self.text_in_font(f, work.size, SAMPLE),
586                None => self.text(TS_BODY | TX_SECONDARY, super::i18n::tr("Keine passende Schrift installiert")),
587            }
588            self.box_end();
589            let fam = self.font_resolved(&work);
590            if !fam.is_empty() && !fam.eq_ignore_ascii_case(&work.family) {
591                let note = format!("{} {}", super::i18n::tr("Angezeigt in:"), fam);
592                self.text(TS_CAPTION | TX_SECONDARY, &note);
593            }
594            self.box_end();
595            let r = self.dialog_buttons(Some(super::i18n::tr("OK")), None, Some(super::i18n::tr("Abbrechen")));
596            if r == DLG_PRIMARY {
597                *sel = p.work.clone();
598                out |= EV_ACT;
599            }
600        }
601        self.window_end();
602        self.id_pop();
603        self.m.fonts.picks.insert(id, p);
604        out
605    }
606
607    // the dialog's styles of family fi: each face, a variable face's named instances (-1: the
608    // four synthetic styles of a family that is not installed)
609    fn font_styles(&mut self, fi: i32) -> Vec<StyleRow> {
610        if fi < 0 {
611            return [(400, false), (400, true), (700, false), (700, true)]
612                .iter()
613                .map(|&(w, it)| StyleRow { face: -1, name: String::from(style_label(w, it)), weight: w, italic: it, axes: Vec::new() })
614                .collect();
615        }
616        let db = self.fonts_db();
617        let mut rows = Vec::new();
618        for k in db.family_range(fi as usize) {
619            let Some(f) = db.face(k) else { continue };
620            let (fw, it, style) = (f.weight as u32, f.flags & (FDFF_ITALIC | FDFF_OBLIQUE) != 0, f.style.clone());
621            let name = if style.is_empty() { String::from(style_label(fw, it)) } else { style };
622            match db.variations(k) {
623                Some(v) if !v.instances.is_empty() => {
624                    // opsz is part of a named instance when the instances differ in it
625                    let ko = v.axes.iter().position(|a| &a.tag == b"opsz");
626                    let opsz_named = ko.is_some_and(|ko| v.instances.iter().any(|i| i.coords.get(ko) != v.instances[0].coords.get(ko)));
627                    for ins in &v.instances {
628                        let val = |tag: &[u8; 4]| v.axes.iter().position(|a| &a.tag == tag).and_then(|i| ins.coords.get(i).copied());
629                        let weight = val(b"wght").map_or(fw, |w| (w + 0.5).clamp(1.0, 1000.0) as u32);
630                        let italic = it || val(b"ital").is_some_and(|x| x >= 0.5) || val(b"slnt").is_some_and(|x| x < 0.0);
631                        let label = if ins.name.is_empty() { String::from(style_label(weight, italic)) } else { ins.name.clone() };
632                        let axes = row_axes(&v.axes, &ins.coords, italic, opsz_named);
633                        rows.push(StyleRow { face: k as i32, name: label, weight, italic, axes });
634                    }
635                }
636                _ => rows.push(StyleRow { face: k as i32, name, weight: fw, italic: it, axes: Vec::new() }),
637            }
638        }
639        rows
640    }
641
642    // the variable face a choice takes (its index), None: not a variable face
643    fn font_var_face(&mut self, sel: &FontSel) -> Option<usize> {
644        let db = self.fonts_db();
645        let (fi, _) = db.find(&sel.family, sel.weight, sel.italic)?;
646        (db.face(fi)?.flags & FDFF_VAR != 0).then_some(fi)
647    }
648
649    // the style row that is exactly the choice (-1: none)
650    fn font_style_match(&mut self, sel: &FontSel, rows: &[StyleRow]) -> i32 {
651        let face = self.font_var_face(sel).map_or(-1, |f| f as i32);
652        let size = Some(sel.size as f32);
653        let db = self.fonts_db();
654        let cur = if face >= 0 { db.var_values(face as usize, sel.weight, sel.italic, size, &sel.axes).map(|v| v.0) } else { None };
655        for (k, r) in rows.iter().enumerate() {
656            let same = match &cur {
657                Some(c) if r.face == face => {
658                    db.var_values(face as usize, r.weight, r.italic, size, &r.axes).is_some_and(|v| v.0.iter().zip(c).all(|(a, b)| (a - b).abs() < 0.01))
659                }
660                Some(_) => false,
661                None => r.weight == sel.weight && r.italic == sel.italic,
662            };
663            if same {
664                return k as i32;
665            }
666        }
667        -1
668    }
669
670    // the dialog's axes of a variable choice: per axis its name, a slider and a number box
671    // (fvar's range); opsz with "Automatisch" (it follows the size)
672    fn font_axes(&mut self, w: &mut FontSel) {
673        let Some(face) = self.font_var_face(w) else { return };
674        let size = Some(w.size as f32);
675        let db = self.fonts_db();
676        let Some(v) = db.variations(face).cloned() else { return };
677        let Some((vals, _)) = db.var_values(face, w.weight, w.italic, size, &w.axes) else { return };
678        self.text(TS_BODY_STRONG, super::i18n::tr("Achsen"));
679        self.id_push_str("axis");
680        self.gridbox_begin(0, &[0, -1, 80, 0], 0, 8);
681        let mut changed: Option<(usize, f32)> = None;
682        for (k, a) in v.axes.iter().enumerate() {
683            if a.hidden {
684                continue;
685            }
686            let rf = axis_range(a);
687            self.text(TS_BODY, &a.name);
688            let mut x = vals[k] as f64;
689            self.next_size(-1, 0);
690            if self.slider_f(self.id_n(k as u32), &mut x, &rf) & EV_ACT != 0 {
691                changed = Some((k, x as f32));
692            }
693            let mut y = vals[k] as f64;
694            self.next_size(-1, 0);
695            if self.number_box_f(self.id_n(k as u32 + 100), &mut y, &rf) & EV_ACT != 0 && y != vals[k] as f64 {
696                changed = Some((k, y as f32));
697            }
698            if &a.tag == b"opsz" {
699                let mut auto = w.axis(*b"opsz").is_none() as i32;
700                if self.checkbox(self.id("opsz_auto"), super::i18n::tr("Automatisch"), &mut auto) & EV_ACT != 0 {
701                    w.set_axis(*b"opsz", if auto != 0 { f32::NAN } else { vals[k] });
702                }
703            } else {
704                self.text(TS_BODY, "");
705            }
706        }
707        self.gridbox_end();
708        self.id_pop();
709        let Some((k, x)) = changed else { return };
710        let a = &v.axes[k];
711        let x = x.clamp(a.min, a.max);
712        match &a.tag {
713            b"wght" => {
714                w.weight = (x + 0.5).clamp(1.0, 1000.0) as u32;
715                w.set_axis(a.tag, if (x - w.weight as f32).abs() < 0.001 { f32::NAN } else { x });
716            }
717            b"opsz" => w.set_axis(a.tag, x),
718            t => {
719                if t == b"ital" {
720                    w.italic = x >= 0.5;
721                } else if t == b"slnt" && !v.axes.iter().any(|b| &b.tag == b"ital") {
722                    w.italic = x < 0.0;
723                }
724                w.set_axis(a.tag, x);
725                // a value the mapping of weight / italic gives needs no entry
726                let face2 = self.font_var_face(w);
727                let db = self.fonts_db();
728                let mut bare = w.clone();
729                bare.set_axis(a.tag, f32::NAN);
730                if let Some(f2) = face2
731                    && let Some((m, _)) = db.var_values(f2, bare.weight, bare.italic, Some(bare.size as f32), &bare.axes)
732                    && (m[k] - x).abs() < 0.001
733                {
734                    w.set_axis(a.tag, f32::NAN);
735                }
736            }
737        }
738    }
739}