git.lucas.co / cce-ui
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 }