GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/lib.rs (12.9K)
1 pub mod color;
2 pub mod widget;
3 pub mod config;
4 pub mod input;
5 pub mod history;
6 pub mod layout;
7 pub mod relief_spec;
8 pub mod wayland;
9 pub mod protocol;
10 pub mod engine;
11 pub mod scale;
12 pub mod units;
13 pub mod backend;
14 pub mod context;
15 pub mod scene;
16 pub mod process;
17 pub mod file_dialog;
18 pub mod icon;
19 pub mod ipc;
20 pub mod mcp;
21 pub mod motion;
22 pub mod vk;
23
24 pub mod colors {
25 pub use crate::color::*;
26 }
27
28 /// The text-shaping library, re-exported so clients need no text dependency of
29 /// their own: `cce_ui::cosmic_text::FontSystem` rather than a per-crate
30 /// `cosmic-text` (previously `glyphon`) entry in every client's Cargo.toml.
31 /// Re-exporting also keeps every client on the one version cce-ui shapes with —
32 /// a `FontSystem` handed across the boundary must be the same type.
33 pub use cosmic_text;
34
35
36 pub static IS_VERTICAL: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
37 pub static BAR_THICKNESS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(24);
38
39 /// `CCE_SCROLL_DEBUG=1` traces the wheel pipeline to stderr: raw coalesced
40 /// axis input (runner), routing decisions (ParametersBg), slider gate/value
41 /// steps, and glide ticks. Diagnostic-only; checked once per process.
42 pub fn scroll_debug() -> bool {
43 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
44 *ON.get_or_init(|| std::env::var_os("CCE_SCROLL_DEBUG").is_some())
45 }
46
47 /// Directory bundled fonts are loaded from: `$CCE_FONTS_DIR`, else `~/Dropbox/Fonts`.
48 /// Resolving via `$HOME` keeps the existing location without a hardcoded username.
49 pub fn fonts_dir() -> String {
50 std::env::var("CCE_FONTS_DIR").unwrap_or_else(|_| {
51 let home = std::env::var("HOME").unwrap_or_default();
52 format!("{home}/Dropbox/Fonts")
53 })
54 }
55
56 /// Directory the bundled cce-icons SVGs are loaded from: `$CCE_ICONS_DIR`, else
57 /// `~/projects/cce/cce-icons/svg`.
58 ///
59 /// The workspace moved out of ~/Dropbox on 2026-08-27: 432k of its 444k files
60 /// were cargo build artifacts, and syncing them kept Dropbox re-hashing a tree
61 /// that regenerates itself. Set `$CCE_ICONS_DIR` if yours lives elsewhere —
62 /// this default is the only path in the toolkit that assumes a checkout
63 /// location.
64 pub fn icons_dir() -> String {
65 std::env::var("CCE_ICONS_DIR").unwrap_or_else(|_| {
66 let home = std::env::var("HOME").unwrap_or_default();
67 format!("{home}/projects/cce/cce-icons/svg")
68 })
69 }
70
71 /// Rasterize a bundled cce-icons SVG (`<name>.svg` under [`icons_dir`]) at
72 /// `px` on its longer side and upload it as a renderer texture. Returns
73 /// `(image id, pixel w, pixel h)` for `PaintCtx::image` / `ImageView`; cached
74 /// per `(name, px)` so widget rebuilds reuse the one upload. `None` when the
75 /// icon is missing or unparsable (callers keep a text fallback).
76 ///
77 /// **The cache is per renderer, not per process.** An image id names an entry
78 /// in one renderer's image table, and a renderer does not outlive its
79 /// session: `window_runner` repairs a lost Wayland transport by opening a new
80 /// session around the same `Application`, which rebuilds the renderer and its
81 /// image table. A draw for an id that table does not hold is skipped rather
82 /// than reported, so a cache that survived the rebuild left every bundled
83 /// glyph in the process silently undrawn — a status bar that reconnected kept
84 /// its numbers and lost its icons, and the same went for every
85 /// [`Button::new_icon`] face, treelist chevron and ramp delete button in the
86 /// DE. Keying the cache on [`vk::renderer_epoch`] makes the first lookup after
87 /// a rebuild a miss, which re-rasterizes and re-uploads into the live
88 /// renderer.
89 ///
90 /// The id cache itself has to stay: this is called from widget rebuilds, so
91 /// uploading per call would burn through the renderer's 256-image budget in
92 /// seconds. Caching only the decode — what [`icon::upload_themed`] does — is
93 /// right for a caller that uploads rarely and owns what it gets back, and
94 /// wrong here.
95 ///
96 /// [`Button::new_icon`]: widget::Button::new_icon
97 pub fn upload_icon(name: &str, px: u32) -> Option<(u32, u32, u32)> {
98 use std::collections::HashMap;
99 use std::sync::Mutex;
100 /// The cached ids, and the renderer epoch they were uploaded to.
101 static CACHE: Mutex<Option<(u32, HashMap<(String, u32), Option<(u32, u32, u32)>>)>> =
102 Mutex::new(None);
103 let epoch = crate::vk::renderer_epoch();
104 let key = (name.to_string(), px);
105 let mut guard = CACHE.lock().unwrap();
106 let (cached_epoch, cache) = guard.get_or_insert_with(|| (epoch, HashMap::new()));
107 if *cached_epoch != epoch {
108 // The renderer these ids named is gone, and its image table went with
109 // it — so this is a forget, not a teardown; there is nothing to free.
110 cache.clear();
111 *cached_epoch = epoch;
112 }
113 if let Some(hit) = cache.get(&key) {
114 return *hit;
115 }
116 let loaded = (|| {
117 let path = format!("{}/{name}.svg", icons_dir());
118 let data = std::fs::read(&path).ok()?;
119 let (rgba, w, h) = rasterize_svg(&data, px)?;
120 Some((crate::vk::upload_rgba(rgba, w, h), w, h))
121 })();
122 cache.insert(key, loaded);
123 loaded
124 }
125
126 /// Rasterize SVG bytes at `px` on the longer side: straight (un-premultiplied)
127 /// RGBA8 pixels plus dimensions, ready for `vk::upload_rgba`. Text elements
128 /// resolve through the shared fontdb (a thread-safe static), so callers may
129 /// rasterize off the UI thread and upload later — cce-files' preview service
130 /// does. `None` when the data is unparsable.
131 pub fn rasterize_svg(data: &[u8], px: u32) -> Option<(Vec<u8>, u32, u32)> {
132 let opt = resvg::usvg::Options::default();
133 let fontdb = crate::widget::get_font_db();
134 let tree = resvg::usvg::Tree::from_data(data, &opt, fontdb).ok()?;
135 let size = tree.size();
136 let (sw, sh) = (size.width().max(1.0), size.height().max(1.0));
137 let scale = px as f32 / sw.max(sh);
138 let w = (sw * scale).round().max(1.0) as u32;
139 let h = (sh * scale).round().max(1.0) as u32;
140 let mut pixmap = resvg::tiny_skia::Pixmap::new(w, h)?;
141 resvg::render(
142 &tree,
143 resvg::tiny_skia::Transform::from_scale(scale, scale),
144 &mut pixmap.as_mut(),
145 );
146 // tiny-skia pixels are premultiplied; the upload path takes straight RGBA.
147 let mut rgba = pixmap.take();
148 for p in rgba.chunks_exact_mut(4) {
149 let a = p[3] as f32 / 255.0;
150 if a > 0.0 {
151 p[0] = ((p[0] as f32 / a).min(255.0)) as u8;
152 p[1] = ((p[1] as f32 / a).min(255.0)) as u8;
153 p[2] = ((p[2] as f32 / a).min(255.0)) as u8;
154 }
155 }
156 Some((rgba, w, h))
157 }
158
159 /// Build a cosmic-text `FontSystem` loaded with the bundled CCE fonts (house style).
160 /// System fonts are loaded only if `$CCE_LOAD_SYSTEM_FONTS` is set. Configured
161 /// custom fonts are validated with a warning if missing.
162 pub fn create_font_system() -> cosmic_text::FontSystem {
163 build_font_system(false)
164 }
165
166 /// A `FontSystem` the TOOLKIT owns, for widget GEOMETRY rather than drawing:
167 /// the shaping a widget's own selection, caret and click-to-index math needs on
168 /// a host that never hands one in.
169 ///
170 /// Paint-walk apps shape through their own (`prepare_text`) and the display
171 /// list shapes through the runner's — but a flat-path host consumes
172 /// `all_quads`, so nothing ever shaped for the widgets it draws and `TextBox`
173 /// fell back to `measure_text_width("M")`: an SVG-rasterized INKED extent, not
174 /// an advance, which walks off the glyphs a few px per character.
175 ///
176 /// Created on FIRST USE, so an app that shapes for itself never pays for it,
177 /// and from the same bundle [`create_font_system`] gives the renderer. The
178 /// shaped-buffer cache behind it is keyed by text/size/family and shared per
179 /// thread, so in practice this reads the very buffers the draw already built.
180 pub fn geometry_font_system() -> &'static std::sync::Mutex<cosmic_text::FontSystem> {
181 static GEOMETRY_FONT_SYSTEM: std::sync::OnceLock<std::sync::Mutex<cosmic_text::FontSystem>> =
182 std::sync::OnceLock::new();
183 GEOMETRY_FONT_SYSTEM.get_or_init(|| std::sync::Mutex::new(create_font_system()))
184 }
185
186 /// Like [`create_font_system`] but always also loads installed system fonts, for
187 /// apps that must see every font on the system (e.g. the font picker) or want
188 /// them as fallbacks. Additive — bundled CCE fonts are still loaded.
189 pub fn create_font_system_with_system_fonts() -> cosmic_text::FontSystem {
190 build_font_system(true)
191 }
192
193 /// Targeted script-fallback faces loaded alongside the bundled house fonts.
194 /// The bundled set covers Latin; anything else shaped to tofu unless
195 /// `$CCE_LOAD_SYSTEM_FONTS` pulled in the entire system set. Probing a short
196 /// list of well-known files keeps startup cheap while giving cosmic-text's
197 /// unix script fallback (family names "Noto Sans CJK *", "Noto Color Emoji")
198 /// real faces to land on. `$CCE_NO_FALLBACK_FONTS` opts out.
199 fn load_fallback_fonts(db: &mut cosmic_text::fontdb::Database) {
200 if std::env::var("CCE_NO_FALLBACK_FONTS").is_ok() {
201 return;
202 }
203 let home = std::env::var("HOME").unwrap_or_default();
204 let candidates = [
205 // CJK (Arch noto-fonts-cjk; Debian/Fedora paths for good measure)
206 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc".to_string(),
207 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc".to_string(),
208 "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc".to_string(),
209 // emoji (Arch noto-fonts-emoji; other distros; per-user install)
210 "/usr/share/fonts/noto/NotoColorEmoji.ttf".to_string(),
211 "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf".to_string(),
212 "/usr/share/fonts/google-noto-emoji-color-fonts/NotoColorEmoji.ttf".to_string(),
213 format!("{home}/.local/share/fonts/NotoColorEmoji.ttf"),
214 ];
215 for path in &candidates {
216 if std::path::Path::new(path).exists() {
217 let _ = db.load_font_file(path);
218 }
219 }
220 }
221
222 fn build_font_system(load_system_fonts: bool) -> cosmic_text::FontSystem {
223 let mut db = cosmic_text::fontdb::Database::new();
224 db.load_fonts_dir(fonts_dir());
225 load_fallback_fonts(&mut db);
226 if load_system_fonts || std::env::var("CCE_LOAD_SYSTEM_FONTS").is_ok() {
227 db.load_system_fonts();
228 }
229 // An empty database guarantees a panic on the first shaped glyph
230 // (cosmic-text: "no default font found"), so if the bundled dir yielded
231 // nothing (missing $HOME/Dropbox/Fonts — e.g. the greeter running as
232 // root), fall back to system fonts rather than crash.
233 if db.faces().next().is_none() {
234 db.load_system_fonts();
235 }
236
237 // Pin the generic families to faces that actually exist. fontdb's defaults
238 // name Windows faces ("Arial"/"Times New Roman"), so Family::SansSerif /
239 // Monospace never resolved here and every glyph of generic-family text
240 // dropped into the per-glyph fallback chain — where Noto Color Emoji sits
241 // high (cosmic-text common_fallback) and hijacked spaces and digits with
242 // emoji metrics. Berkeley Mono is the house mono; Noto Sans CJK SC (the
243 // targeted fallback face above) doubles as a full Latin sans.
244 fn has_family(db: &cosmic_text::fontdb::Database, fam: &str) -> bool {
245 db.faces()
246 .any(|f| f.families.iter().any(|(n, _)| n == fam))
247 }
248 if has_family(&db, "Berkeley Mono") {
249 db.set_monospace_family("Berkeley Mono");
250 }
251 if has_family(&db, "Noto Sans CJK SC") {
252 db.set_sans_serif_family("Noto Sans CJK SC");
253 }
254
255 // Validate configured custom fonts
256 let font_getters = vec![
257 ("list_font", crate::layout::list_font_parsed().0),
258 ("menubar_font", crate::layout::menubar_font_parsed().0),
259 ("statusbar_font", crate::layout::statusbar_font_parsed().0),
260 ("font_selector_font", crate::layout::font_selector_font_parsed().0),
261 ("button_strip_font", crate::layout::button_strip_font_parsed().0),
262 ("control_label_font", crate::layout::control_label_font_parsed().0),
263 ("control_label_font_detached", crate::layout::control_label_font_detached_parsed().0),
264 ("tree_font", crate::layout::tree_font_parsed().0),
265 ("graph_font", crate::layout::graph_font_parsed().0),
266 ("graph_node_font", crate::layout::graph_node_font_parsed().0),
267 ];
268
269 for (name, family) in font_getters {
270 if !family.is_empty() && family != "Berkeley Mono" && family != "sans-serif" {
271 let mut found = false;
272 for face in db.faces() {
273 for (fam, _) in &face.families {
274 if fam.to_lowercase() == family.to_lowercase() {
275 found = true;
276 break;
277 }
278 }
279 if found {
280 break;
281 }
282 }
283 if !found {
284 eprintln!(
285 "WARNING: Configured font family '{}' for property '{}' was not found in the fonts database. Falling back to default font.",
286 family, name
287 );
288 }
289 }
290 }
291
292 cosmic_text::FontSystem::new_with_locale_and_db("en-US".to_string(), db)
293 }