khora_core/ui/fonts.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Backend-agnostic font configuration for the editor shell.
16//!
17//! Apps can supply their own typefaces (proportional + monospace) via the
18//! [`FontPack`] type. The shell hands them off to its UI backend (egui, etc.).
19//!
20//! Fonts are entirely optional: if a [`FontPack`] is never provided, the
21//! backend falls back to its built-in defaults.
22
23/// A bundled font, either as a static slice or owned bytes loaded at runtime.
24#[derive(Debug, Clone)]
25pub enum FontHandle {
26 /// Font data bundled into the binary via `include_bytes!`.
27 Static(&'static [u8]),
28 /// Font data read from disk or fetched at runtime.
29 Owned(Vec<u8>),
30}
31
32impl FontHandle {
33 /// Returns the underlying byte slice.
34 pub fn as_bytes(&self) -> &[u8] {
35 match self {
36 Self::Static(s) => s,
37 Self::Owned(v) => v.as_slice(),
38 }
39 }
40}
41
42/// A named font face inside a pack. The name is used by the backend to
43/// register the font in its registry and (where applicable) to make it
44/// addressable by user code.
45#[derive(Debug, Clone)]
46pub struct NamedFont {
47 /// Stable identifier (e.g. `"geist-regular"`, `"geist-mono"`).
48 pub name: String,
49 /// The raw font bytes.
50 pub data: FontHandle,
51}
52
53/// A collection of fonts grouped by family.
54///
55/// The first font in each `Vec` is the *primary* face — the one the backend
56/// installs as the default for that family. Subsequent fonts act as
57/// fallbacks (e.g. for missing glyph ranges).
58#[derive(Debug, Clone, Default)]
59pub struct FontPack {
60 /// Proportional / sans-serif faces. The first one becomes the default
61 /// proportional font.
62 pub proportional: Vec<NamedFont>,
63 /// Monospaced faces. The first one becomes the default monospace font.
64 pub monospace: Vec<NamedFont>,
65 /// Display / serif faces (Fraunces / …). The first one is exposed by the
66 /// backend under the family name `"display"` so widgets can reach it via
67 /// [`FontFamilyHint::Display`](super::editor::ui_builder::FontFamilyHint::Display).
68 /// When empty, the backend aliases `"display"` to the proportional family.
69 pub display: Vec<NamedFont>,
70 /// Icon faces (Lucide / Phosphor / …). The first one is exposed by the
71 /// backend under the family name `"icons"` so widgets can reach it via
72 /// [`FontFamilyHint::Icons`](super::editor::ui_builder::FontFamilyHint::Icons).
73 pub icons: Vec<NamedFont>,
74}
75
76impl FontPack {
77 /// Returns `true` if no fonts were specified — backends should leave
78 /// their defaults untouched in that case.
79 pub fn is_empty(&self) -> bool {
80 self.proportional.is_empty()
81 && self.monospace.is_empty()
82 && self.display.is_empty()
83 && self.icons.is_empty()
84 }
85}