git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit66280ef9d3a6215ac491098fbe7924e1da9a961e
parent4f58eed592
authorLucas Galante <[email protected]>
date2026-08-13 23:45
deps: depend on cosmic-text directly, dropping glyphon and wgpu

glyphon is a wgpu text renderer. The wgpu path was retired in favour of raw
Vulkan (src/vk/), so none of glyphon's own API was reachable any more — the
five TextRenderer/TextArea mentions left in the tree are doc comments
describing what the Vulkan path mirrors. Every `glyphon::` item actually used
here (FontSystem, Buffer, Attrs, Metrics, Family, Weight, Style, Shaping,
Color, SwashCache, cosmic_text::Align, fontdb::Database) was a cosmic-text
re-export, so the dependency is now cosmic-text directly.

The one exception was `glyphon::TextBounds`, glyphon's own type, used solely
as an intermediate on the way to `TextSpan::bounds`; it is now a plain
four-i32 struct in backend/window_runner.rs.

cosmic-text is pinned to =0.12.1, the exact version glyphon 0.8 resolved to,
so shaping cannot drift. Verified unchanged three ways:
  - the resolved feature set of the whole shaping stack (cosmic-text, fontdb,
    rustybuzz, swash, ttf-parser) is byte-identical either way;
  - shaping the same strings via glyphon's re-export and via cosmic-text
    directly gives identical glyph ids, subpixel advances and line widths
    over 288 runs (4 sizes x 4 families x 3 weight/style combos);
  - cce-files rendered pixel-identical before and after over 4.26M pixels.

Also re-exports cosmic_text from the crate root, so clients need no text
dependency of their own and cannot drift onto a second FontSystem type.

Drops glyphon, wgpu, wgpu-core, wgpu-hal, wgpu-types, metal, gpu-alloc,
gpu-descriptor, khronos-egl and 10 more from the build (19 crates). The rest
of wgpu's tree stays only because naga/ash are direct dependencies here.
Clean build of this crate's gpu+text dependency set: 24.9s -> 18.0s wall,
5m05s -> 3m26s CPU.

Co-Authored-By: Claude <[email protected]>

 CLAUDE.md                        | 12 ++++---
 Cargo.toml                       | 11 +++++--
 src/backend/window_runner.rs     | 67 +++++++++++++++++++++++-----------------
 src/lib.rs                       | 23 +++++++++-----
 src/main.rs                      |  2 +-
 src/scene/layout.rs              |  2 +-
 src/scene/paint.rs               |  8 ++---
 src/vk/mod.rs                    |  4 +--
 src/vk/renderer.rs               |  6 ++--
 src/vk/text.rs                   | 22 ++++++-------
 src/widget/container/menu.rs     |  2 +-
 src/widget/container/treelist.rs |  2 +-
 src/widget/display/label.rs      | 12 +++----
 src/widget/display/list_item.rs  | 10 +++---
 src/widget/display/status_bar.rs |  8 ++---
 src/widget/display/text_label.rs |  2 +-
 src/widget/input/text_box.rs     |  2 +-
 src/widget/mod.rs                |  2 +-
 src/widget/model.rs              |  4 +--
 19 files changed, 114 insertions(+), 87 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index dcf3563..8c17b46 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,8 +17,11 @@ set outright.
 - **Transport**: raw `wayland-client` 0.31 + `smithay-client-toolkit` 0.19, driven by a `calloop`
   event loop. Clients are real Wayland surfaces (xdg toplevels, xdg popups, and `wlr-layer-shell`
   layer surfaces), not toolkit-owned windows.
-- **Rendering**: **wgpu 24** for all geometry through a single `src/shader.wgsl` pipeline, and
-  **glyphon** for text. There is no HTML/DOM — the UI is GPU primitives (quads, rounded rects with
+- **Rendering**: raw Vulkan via **ash** (`src/vk/`, `VkRenderer`) for all geometry, and
+  **cosmic-text** + swash for text (shaped into a self-managed glyph atlas by `src/vk/text.rs`).
+  The wgpu path is retired; cosmic-text used to be reached through **glyphon**, which is gone
+  too — every `glyphon::` item used here was a cosmic-text re-export, and dropping it takes
+  wgpu out of the build. There is no HTML/DOM — the UI is GPU primitives (quads, rounded rects with
   per-corner radii, vectors with caps, arcs, circles, bevels). Tessellators live in
   `backend/window_runner.rs` and are re-exported through `src/engine.rs`.
 - It is **both a library and a binary.** `src/lib.rs` is the toolkit; `src/main.rs` is
@@ -125,7 +128,7 @@ cce-system-interface) to confirm behavior, not just the test suite.
 ## Module map (where things live)
 
 - `layout.rs` (largest file, ~5.7k lines) — fonts + sizing; many `*_font_parsed()` getters and
-  the `read_preferred_fonts` / font-family resolution used by the glyphon text path.
+  the `read_preferred_fonts` / font-family resolution used by the cosmic-text path.
 - `color.rs` — color model and named colors (`colors` re-export module in `lib.rs`).
 - `config.rs` — KDL loading and `kdl_to_json` conversion (see workspace `CLAUDE.md` for paths).
 - `context.rs` — `UiContext`: the retained widget tree, event routing, spatial grid, dirty
@@ -143,7 +146,8 @@ cce-system-interface) to confirm behavior, not just the test suite.
 
 ## Fonts & assets
 
-`lib.rs` builds the glyphon `FontSystem`. Bundled fonts load from `$CCE_FONTS_DIR` (else
+`lib.rs` builds the cosmic-text `FontSystem` (re-exported as `cce_ui::cosmic_text` so clients
+need no text dependency of their own). Bundled fonts load from `$CCE_FONTS_DIR` (else
 `~/Dropbox/Fonts`); bundled icons from `$CCE_ICONS_DIR` (else `~/Dropbox/cce/cce-icons/svg`).
 System fonts are loaded only when `$CCE_LOAD_SYSTEM_FONTS` is set (or via
 `create_font_system_with_system_fonts()`, used by the font picker). Configured custom font
diff --git a/Cargo.toml b/Cargo.toml
index b81e0c6..3c81270 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,8 +11,15 @@ path = "src/lib.rs"
 raw-window-handle = "0.6"
 glam = "0.29"
 bytemuck = { version = "1", features = ["derive"] }
-glyphon = "0.8"
-# Raw-Vulkan backend (versions match what wgpu 24 already pulls in).
+# Text shaping. This was reached through glyphon until the wgpu path was retired;
+# every `glyphon::` item cce-ui ever used was a cosmic-text re-export (the sole
+# exception, `TextBounds`, is now defined in backend/window_runner.rs), so depending
+# on cosmic-text directly drops glyphon, wgpu and 17 more crates from the build —
+# the rest of wgpu's tree stays only because naga/ash below already pull it.
+# Pinned to the exact version glyphon 0.8 resolved to, so shaping does not change:
+# verified by diffing shaped glyph ids/advances both ways over 288 runs.
+cosmic-text = "=0.12.1"
+# Raw-Vulkan backend (versions match what wgpu 24 already pulled in).
 ash = "0.38"
 gpu-allocator = { version = "0.27", default-features = false, features = ["vulkan"] }
 naga = { version = "24", features = ["wgsl-in", "spv-out"] }
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 0fcb0f0..d60695b 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -35,10 +35,7 @@ pub use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_to
 pub use smithay_client_toolkit::seat::pointer::CursorIcon as PointerCursorIcon;
 use calloop::EventLoop;
 use calloop_wayland_source::WaylandSource;
-use glyphon::{
-    FontSystem,
-    TextBounds, Buffer, Attrs, Metrics,
-};
+use cosmic_text::{FontSystem, Buffer, Attrs, Metrics};
 use crate::widget::{WidgetHost, TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey, Position};
 use crate::wayland::detect_scale_factor;
 use crate::vk::{Batch2D, Frame2D, TextSpan, VkRenderer};
@@ -153,57 +150,57 @@ pub fn get_text_buffer_attrs(
             "monospace" => {
                 if !mono_fallback.is_empty() {
                     if let Some(ref cased) = resolved_storage {
-                        glyphon::Family::Name(cased)
+                        cosmic_text::Family::Name(cased)
                     } else {
-                        glyphon::Family::Name(crate::layout::get_system_monospace_font())
+                        cosmic_text::Family::Name(crate::layout::get_system_monospace_font())
                     }
                 } else {
-                    glyphon::Family::Name(crate::layout::get_system_monospace_font())
+                    cosmic_text::Family::Name(crate::layout::get_system_monospace_font())
                 }
             }
             "sans-serif" => {
                 if !sans_fallback.is_empty() {
                     if let Some(ref cased) = resolved_storage {
-                        glyphon::Family::Name(cased)
+                        cosmic_text::Family::Name(cased)
                     } else {
-                        glyphon::Family::SansSerif
+                        cosmic_text::Family::SansSerif
                     }
                 } else {
-                    glyphon::Family::SansSerif
+                    cosmic_text::Family::SansSerif
                 }
             }
             "serif" => {
                 if !serif_fallback.is_empty() {
                     if let Some(ref cased) = resolved_storage {
-                        glyphon::Family::Name(cased)
+                        cosmic_text::Family::Name(cased)
                     } else {
-                        glyphon::Family::Serif
+                        cosmic_text::Family::Serif
                     }
                 } else {
-                    glyphon::Family::Serif
+                    cosmic_text::Family::Serif
                 }
             }
-            name => glyphon::Family::Name(name),
+            name => cosmic_text::Family::Name(name),
         }
     } else {
         if !sans_fallback.is_empty() {
             if let Some(ref cased) = resolved_sans {
-                glyphon::Family::Name(cased)
+                cosmic_text::Family::Name(cased)
             } else {
-                glyphon::Family::SansSerif
+                cosmic_text::Family::SansSerif
             }
         } else {
-            glyphon::Family::SansSerif
+            cosmic_text::Family::SansSerif
         }
     };
     attrs = attrs.family(family);
     if text_attrs.italic {
-        attrs = attrs.style(glyphon::Style::Italic);
+        attrs = attrs.style(cosmic_text::Style::Italic);
     }
     if let Some(w) = text_attrs.weight {
-        attrs = attrs.weight(glyphon::Weight(w));
+        attrs = attrs.weight(cosmic_text::Weight(w));
     }
-    buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
+    buf.set_text(fs, text, attrs, cosmic_text::Shaping::Advanced);
     buf.shape_until_scroll(fs, true);
 
     BUFFER_CACHE.with(|cache| {
@@ -260,9 +257,9 @@ pub fn get_text_buffer_laid_out(
     buf.set_size(fs, layout.wrap_width.map(|w| w * scale), Some(layout.box_height * scale));
 
     let align = match layout.align_h {
-        AlignH::Left => glyphon::cosmic_text::Align::Left,
-        AlignH::Center => glyphon::cosmic_text::Align::Center,
-        AlignH::Right => glyphon::cosmic_text::Align::Right,
+        AlignH::Left => cosmic_text::Align::Left,
+        AlignH::Center => cosmic_text::Align::Center,
+        AlignH::Right => cosmic_text::Align::Right,
     };
     for line in &mut buf.lines {
         line.set_align(Some(align));
@@ -280,6 +277,18 @@ pub fn get_text_buffer_laid_out(
     (buf, voff)
 }
 
+/// A text item's clip rect in physical pixels. This was `glyphon::TextBounds` — the one
+/// glyphon-owned type cce-ui ever used, everything else being a cosmic-text re-export — so
+/// it is defined here now that the dependency is cosmic-text directly. Same plain
+/// four-`i32` layout; it is only an intermediate on the way to `TextSpan::bounds`.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct TextBounds {
+    pub left: i32,
+    pub top: i32,
+    pub right: i32,
+    pub bottom: i32,
+}
+
 /// The popover-occlusion clamp shared by the default [`Application::text_areas`] mapping and
 /// the display-list text path: clip a text item's bounds so it does not bleed through an open
 /// popover's plate. A text item whose own bounds coincide with a popover rect IS that popover's
@@ -2576,7 +2585,7 @@ pub trait Application: Sized + 'static {
         None
     }
 
-    /// Opt in to render the display list's `Prim::Text` items through the glyphon pass
+    /// Opt in to render the display list's `Prim::Text` items through the glyph pass
     /// (shaped via the shared buffer cache, clipped to the item clip ∩ the prim bounds). An
     /// app's ENTIRE frame — geometry and text — is then one
     /// [`display_list`](Application::display_list). Default `false` draws no text (an app that
@@ -2723,7 +2732,7 @@ pub struct EngineState<A: Application> {
     
     pub renderer: Option<VkRenderer>,
     pub font_system: Option<FontSystem>,
-    pub swash_cache: glyphon::SwashCache,
+    pub swash_cache: cosmic_text::SwashCache,
     
     pub scale_factor: f64,
     /// The buffer scale last sent to the surface. Updated in [`Self::render`],
@@ -2797,7 +2806,7 @@ pub struct EngineState<A: Application> {
     /// runs the off-screen hover-clear (which would otherwise corrupt the
     /// drag: a ramp key snapped to the graph corner).
     pub buttons_down: u32,
-    /// This frame's display-list text, shaped and held here so the glyphon `TextArea`s built
+    /// This frame's display-list text, shaped and held here so the `TextSpan`s built
     /// in the render pass can borrow the buffers (Phase 6 —
     /// [`Application::display_list_text`]).
     pub dl_text_items: Vec<TextItem>,
@@ -2985,7 +2994,7 @@ impl<A: Application> EngineState<A> {
             .unwrap_or_else(|| crate::scene::paint::PaintCtx::new().finish());
 
         // 1a. Phase 6 display-list text: shape the list's Text prims through the shared buffer
-        // cache and hold them for the glyphon pass (the TextAreas built below borrow these).
+        // cache and hold them for the glyph pass (the TextSpans built below borrow these).
         // Clip = the paint walk's item clip ∩ the prim's own bounds, in logical space.
         self.dl_text_items.clear();
         if self.inner.as_ref().unwrap().display_list_text() {
@@ -3008,7 +3017,7 @@ impl<A: Application> EngineState<A> {
                         buffer,
                         x: *x,
                         y: *y + y_off,
-                        color: glyphon::Color::rgba(
+                        color: cosmic_text::Color::rgba(
                             color[0],
                             color[1],
                             color[2],
@@ -4312,7 +4321,7 @@ fn run_session<'l, A: Application>(
         inner: None,
         renderer: None,
         font_system: None,
-        swash_cache: glyphon::SwashCache::new(),
+        swash_cache: cosmic_text::SwashCache::new(),
         scale_factor: 1.0,
         committed_buffer_scale: 1,
         entered_outputs: Vec::new(),
diff --git a/src/lib.rs b/src/lib.rs
index 914dd6e..f9e6b54 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -20,6 +20,13 @@ pub mod colors {
     pub use crate::color::*;
 }
 
+/// The text-shaping library, re-exported so clients need no text dependency of
+/// their own: `cce_ui::cosmic_text::FontSystem` rather than a per-crate
+/// `cosmic-text` (previously `glyphon`) entry in every client's Cargo.toml.
+/// Re-exporting also keeps every client on the one version cce-ui shapes with —
+/// a `FontSystem` handed across the boundary must be the same type.
+pub use cosmic_text;
+
 
 pub static IS_VERTICAL: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
 pub static BAR_THICKNESS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(24);
@@ -109,17 +116,17 @@ pub fn rasterize_svg(data: &[u8], px: u32) -> Option<(Vec<u8>, u32, u32)> {
     Some((rgba, w, h))
 }
 
-/// Build a glyphon `FontSystem` loaded with the bundled CCE fonts (house style).
+/// Build a cosmic-text `FontSystem` loaded with the bundled CCE fonts (house style).
 /// System fonts are loaded only if `$CCE_LOAD_SYSTEM_FONTS` is set. Configured
 /// custom fonts are validated with a warning if missing.
-pub fn create_font_system() -> glyphon::FontSystem {
+pub fn create_font_system() -> cosmic_text::FontSystem {
     build_font_system(false)
 }
 
 /// Like [`create_font_system`] but always also loads installed system fonts, for
 /// apps that must see every font on the system (e.g. the font picker) or want
 /// them as fallbacks. Additive — bundled CCE fonts are still loaded.
-pub fn create_font_system_with_system_fonts() -> glyphon::FontSystem {
+pub fn create_font_system_with_system_fonts() -> cosmic_text::FontSystem {
     build_font_system(true)
 }
 
@@ -129,7 +136,7 @@ pub fn create_font_system_with_system_fonts() -> glyphon::FontSystem {
 /// list of well-known files keeps startup cheap while giving cosmic-text's
 /// unix script fallback (family names "Noto Sans CJK *", "Noto Color Emoji")
 /// real faces to land on. `$CCE_NO_FALLBACK_FONTS` opts out.
-fn load_fallback_fonts(db: &mut glyphon::cosmic_text::fontdb::Database) {
+fn load_fallback_fonts(db: &mut cosmic_text::fontdb::Database) {
     if std::env::var("CCE_NO_FALLBACK_FONTS").is_ok() {
         return;
     }
@@ -152,8 +159,8 @@ fn load_fallback_fonts(db: &mut glyphon::cosmic_text::fontdb::Database) {
     }
 }
 
-fn build_font_system(load_system_fonts: bool) -> glyphon::FontSystem {
-    let mut db = glyphon::cosmic_text::fontdb::Database::new();
+fn build_font_system(load_system_fonts: bool) -> cosmic_text::FontSystem {
+    let mut db = cosmic_text::fontdb::Database::new();
     db.load_fonts_dir(fonts_dir());
     load_fallback_fonts(&mut db);
     if load_system_fonts || std::env::var("CCE_LOAD_SYSTEM_FONTS").is_ok() {
@@ -174,7 +181,7 @@ fn build_font_system(load_system_fonts: bool) -> glyphon::FontSystem {
     // high (cosmic-text common_fallback) and hijacked spaces and digits with
     // emoji metrics. Berkeley Mono is the house mono; Noto Sans CJK SC (the
     // targeted fallback face above) doubles as a full Latin sans.
-    fn has_family(db: &glyphon::cosmic_text::fontdb::Database, fam: &str) -> bool {
+    fn has_family(db: &cosmic_text::fontdb::Database, fam: &str) -> bool {
         db.faces()
             .any(|f| f.families.iter().any(|(n, _)| n == fam))
     }
@@ -222,7 +229,7 @@ fn build_font_system(load_system_fonts: bool) -> glyphon::FontSystem {
         }
     }
 
-    glyphon::FontSystem::new_with_locale_and_db("en-US".to_string(), db)
+    cosmic_text::FontSystem::new_with_locale_and_db("en-US".to_string(), db)
 }
 
 
diff --git a/src/main.rs b/src/main.rs
index f0fd643..949fdc2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,7 @@
 //! 1. **One paint path.** The whole frame — geometry AND text — is built in
 //!    [`Application::display_list`] as prims on a [`PaintCtx`], with
 //!    [`Application::display_list_text`] returning `true`. There is no `view*`/`text_items`
-//!    pair, no app-side `FontSystem`, no glyphon buffers: text is a `Prim::Text` shaped by
+//!    pair, no app-side `FontSystem`, no cosmic-text buffers: text is a `Prim::Text` shaped by
 //!    the engine's shared cache.
 //! 2. **Layout via the scene solver.** The frame is a plain `Arena<LayoutBox>` tree the
 //!    app builds and solves with [`compute_layout`]; widgets get their rects from the
diff --git a/src/scene/layout.rs b/src/scene/layout.rs
index d496d9d..84e1136 100644
--- a/src/scene/layout.rs
+++ b/src/scene/layout.rs
@@ -20,7 +20,7 @@
 //! Layout modes ([`LayoutMode`]): **Flex** (row/column with grow/shrink, gap, padding, main/cross
 //! alignment incl. stretch), **Stack** (Z-overlay with per-axis alignment), and **Grid** (fixed
 //! column count with uniform column width and per-row heights). Deferred to later increments:
-//! wrapping, percentage lengths, width-dependent adaptive grids, and the glyphon text-measure
+//! wrapping, percentage lengths, width-dependent adaptive grids, and the cosmic-text text-measure
 //! hook for real leaf widgets (that lands with Phase 2b integration).
 
 use crate::scene::arena::{Arena, NodeId};
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index c814b0e..079ff50 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -114,7 +114,7 @@ pub enum Prim {
     ConcaveFillet { cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool },
     /// Text in sRGB u8 (the `TextLabel` convention). `font` is a font string for
     /// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
-    /// for the glyph pass (Phase 6: the backend renders these through glyphon when the app
+    /// for the glyph pass (Phase 6: the backend renders these through the glyph pass when the app
     /// opts in via `Application::display_list_text`; the paint walk's clip additionally
     /// applies through the item's `clip`). `attrs` carries the optional shaping attributes
     /// beyond family+size (the font picker's italic/weight preview variants). `layout`, when
@@ -130,7 +130,7 @@ pub enum Prim {
 }
 
 /// Horizontal alignment of laid-out (boxed) text — the toolkit-plain mirror of
-/// `glyphon::cosmic_text::Align`, mapped at shape time.
+/// `cosmic_text::Align`, mapped at shape time.
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
 pub enum AlignH {
     #[default]
@@ -162,8 +162,8 @@ pub struct TextLayout {
 
 /// Optional shaping attributes for a [`Prim::Text`] — the subset a widget can request beyond
 /// family + size. `weight` is the OpenType weight (400 regular, 700 bold); `None` leaves the
-/// family default. Kept toolkit-plain (no glyphon types) like the rest of the scene layer;
-/// the backend maps them onto `glyphon::Style`/`Weight` at shape time.
+/// family default. Kept toolkit-plain (no cosmic-text types) like the rest of the scene layer;
+/// the backend maps them onto `cosmic_text::Style`/`Weight` at shape time.
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
 pub struct TextAttrs {
     pub italic: bool,
diff --git a/src/vk/mod.rs b/src/vk/mod.rs
index 698a1ee..a40a82f 100644
--- a/src/vk/mod.rs
+++ b/src/vk/mod.rs
@@ -11,8 +11,8 @@
 //!   quads with circle clipping, the wavy-blob sentinel, window-corner
 //!   rounding (radius 0 disables), and blur-behind plates (negative alpha)
 //!   sampling the renderer-managed backdrop.
-//! - **Text** (`glyph.wgsl` + [`TextSpan`]): cosmic-text shaping (via glyphon's
-//!   re-export) + swash rasterization into a self-managed glyph atlas, with
+//! - **Text** (`glyph.wgsl` + [`TextSpan`]): cosmic-text shaping (a direct dependency since the
+//!   wgpu path retired) + swash rasterization into a self-managed glyph atlas, with
 //!   per-span bounds clipping, rotation, and circle clipping.
 //! - **3D** (`scene3d.wgsl` + [`SceneDraw`]): handle-based [`Vertex3D`] meshes
 //!   with a depth buffer, rendered into the full-size backdrop image
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index a7a9423..5c5c9b6 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -1422,11 +1422,11 @@ impl VkRenderer {
 
     /// Stage text for the next `draw_frame`: shape-cache misses are rasterized
     /// into the glyph atlas and vertices are built against the current extent.
-    /// Mirrors `glyphon::TextRenderer::prepare`.
+    /// Mirrors what was `glyphon::TextRenderer::prepare`.
     pub fn prepare_text(
         &mut self,
-        font_system: &mut glyphon::FontSystem,
-        swash_cache: &mut glyphon::SwashCache,
+        font_system: &mut cosmic_text::FontSystem,
+        swash_cache: &mut cosmic_text::SwashCache,
         spans: &[TextSpan<'_>],
     ) {
         // Against the extent this frame will actually be drawn at: `resize` is
diff --git a/src/vk/text.rs b/src/vk/text.rs
index 24af1ca..2210cb9 100644
--- a/src/vk/text.rs
+++ b/src/vk/text.rs
@@ -1,11 +1,11 @@
-//! Text on ash: cosmic-text shaping (reached through glyphon's re-export, so the
-//! shaping behavior and fonts are byte-identical to the wgpu path) + swash
-//! rasterization into a self-managed RGBA glyph atlas, drawn by the glyph.wgsl
-//! pipeline inside the renderer's render pass.
+//! Text on ash: cosmic-text shaping + swash rasterization into a self-managed
+//! RGBA glyph atlas, drawn by the glyph.wgsl pipeline inside the renderer's
+//! render pass. (cosmic-text used to be reached through glyphon's re-export;
+//! the dependency is direct now that the wgpu path is gone, pinned to the same
+//! version, so shaping behavior and fonts are unchanged.)
 //!
-//! `TextSpan` mirrors `glyphon::TextArea` (buffer + position + scale + bounds +
-//! default color) so the eventual cutover from `text_renderer.prepare(...)` is
-//! mechanical.
+//! `TextSpan` mirrors what was `glyphon::TextArea` (buffer + position + scale +
+//! bounds + default color) — the shape the wgpu-era cutover was written against.
 //!
 //! Atlas strategy: shelf packing into a 1024² RGBA8 image with a CPU mirror.
 //! When new glyphs land, the whole mirror is re-uploaded before the next render
@@ -22,8 +22,8 @@ use gpu_allocator::vulkan::{
 };
 use gpu_allocator::MemoryLocation;
 
-use glyphon::cosmic_text::{Buffer as TextBuffer, CacheKey, SwashContent};
-use glyphon::{FontSystem, SwashCache};
+use cosmic_text::{Buffer as TextBuffer, CacheKey, SwashContent};
+use cosmic_text::{FontSystem, SwashCache};
 
 use super::renderer::{create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
 
@@ -32,7 +32,7 @@ const ATLAS_PAD: u32 = 1;
 
 /// One shaped text run to draw. `left`/`top` are physical pixels and `scale`
 /// multiplies the shaped (logical) glyph positions — the same contract as
-/// glyphon::TextArea, where callers pass `label.x * scale`.
+/// the old glyphon::TextArea, where callers pass `label.x * scale`.
 pub struct TextSpan<'a> {
     pub buffer: &'a TextBuffer,
     pub left: f32,
@@ -543,7 +543,7 @@ impl TextStage {
                         continue;
                     }
 
-                    // glyphon's placement formula, physical pixels.
+                    // glyphon's placement formula (kept verbatim), physical pixels.
                     let mut x0 = (physical.x + entry.left) as f32;
                     let mut y0 = (line_y + physical.y - entry.top) as f32;
                     let mut x1 = x0 + entry.w as f32;
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 15c4390..3154a1b 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -12,7 +12,7 @@
 //! zero constructors workspace-wide (dead code).
 //!
 //! Dropped with the migration: the `title_buf`/`curved_title_char_bufs`/`context_item_bufs`
-//! glyphon caches — `get_text_items` always returned empty, so `prepare_text` built buffers
+//! cosmic-text caches — `get_text_items` always returned empty, so `prepare_text` built buffers
 //! nothing ever read (an abandoned optimization). Also gone: the legacy `rect()` override's
 //! vertical-mode dynamic height (`with_vertical` has no callers workspace-wide; the vertical
 //! label/title geometry is kept for the strip's rotated mode, but the widget rect is the
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index d87eb3a..3462103 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -716,7 +716,7 @@ impl Paint for TreeList {
         true
     }
 
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
+    fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
         self.search_box.prepare_text(fs);
         self.add_key_btn.prepare_text(fs);
         if self.add_key_popover_open {
diff --git a/src/widget/display/label.rs b/src/widget/display/label.rs
index 50ee8ae..80a64f9 100644
--- a/src/widget/display/label.rs
+++ b/src/widget/display/label.rs
@@ -117,10 +117,10 @@ mod tests {
 // Styled label builder with optional strikethrough
 #[derive(Debug)]
 pub struct StyledLabel {
-    pub buffer: glyphon::Buffer,
+    pub buffer: cosmic_text::Buffer,
     pub w: f32,
     pub color: [f32; 4],
-    pub g_color: glyphon::Color,
+    pub g_color: cosmic_text::Color,
     pub strikethrough: bool,
     pub strikethrough_color: Option<[f32; 4]>,
     // Source retained so the label can be re-emitted as a display-list Text prim (Phase 6ak):
@@ -146,11 +146,11 @@ pub struct LabelPrim {
 }
 
 impl StyledLabel {
-    pub fn new(fs: &mut glyphon::FontSystem, text: &str, size: f32, color: [f32; 4]) -> Self {
+    pub fn new(fs: &mut cosmic_text::FontSystem, text: &str, size: f32, color: [f32; 4]) -> Self {
         Self::new_with_family(fs, text, size, color, "sans-serif")
     }
 
-    pub fn new_with_family(fs: &mut glyphon::FontSystem, text: &str, size: f32, color: [f32; 4], family: &str) -> Self {
+    pub fn new_with_family(fs: &mut cosmic_text::FontSystem, text: &str, size: f32, color: [f32; 4], family: &str) -> Self {
         let scale = crate::scale::scale_factor();
         let mut final_text = text.to_string();
         let is_vert = crate::IS_VERTICAL.load(std::sync::atomic::Ordering::Relaxed);
@@ -162,7 +162,7 @@ impl StyledLabel {
             let bar_thickness = crate::BAR_THICKNESS.load(std::sync::atomic::Ordering::Relaxed) as f32;
             buffer.set_size(fs, Some(bar_thickness * scale as f32), None);
             for line in &mut buffer.lines {
-                line.set_align(Some(glyphon::cosmic_text::Align::Center));
+                line.set_align(Some(cosmic_text::Align::Center));
             }
             buffer.shape_until_scroll(fs, true);
         }
@@ -171,7 +171,7 @@ impl StyledLabel {
             let num_lines = buffer.layout_runs().count();
             w = num_lines as f32 * size * 1.05;
         }
-        let g_color = glyphon::Color::rgb(
+        let g_color = cosmic_text::Color::rgb(
             (color[0] * 255.0) as u8,
             (color[1] * 255.0) as u8,
             (color[2] * 255.0) as u8,
diff --git a/src/widget/display/list_item.rs b/src/widget/display/list_item.rs
index 4164287..8ae0ba6 100644
--- a/src/widget/display/list_item.rs
+++ b/src/widget/display/list_item.rs
@@ -1,4 +1,4 @@
-//! `TextItem` (retained glyphon buffer holder, unchanged) and the narrow-trait
+//! `TextItem` (retained cosmic-text buffer holder, unchanged) and the narrow-trait
 //! `InteractiveListItem` (Phase 5j): Button-style press/release with themed
 //! selected/hover/press overlays and title/subtitle text.
 
@@ -9,10 +9,10 @@ use crate::widget::{Adapted, ElementState, Event, EventCtx, Input, Layout, Mouse
 
 #[derive(Debug, Clone)]
 pub struct TextItem {
-    pub buffer: glyphon::Buffer,
+    pub buffer: cosmic_text::Buffer,
     pub x: f32,
     pub y: f32,
-    pub color: glyphon::Color,
+    pub color: cosmic_text::Color,
     pub bounds: Option<[f32; 4]>,
     /// Optional circular clip `[cx, cy, r]` in logical pixels (a display-list item's
     /// `clip_circle` carried through to the glyph pass). `None` for ordinary labels.
@@ -24,12 +24,12 @@ pub struct TextItem {
 
 impl TextItem {
     pub fn new(
-        fs: &mut glyphon::FontSystem,
+        fs: &mut cosmic_text::FontSystem,
         text: &str,
         size: f32,
         x: f32,
         y: f32,
-        color: glyphon::Color,
+        color: cosmic_text::Color,
         font: Option<&str>,
         bounds: Option<[f32; 4]>,
     ) -> Self {
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index 7b7f43b..bb86254 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -3,7 +3,7 @@
 //! statusbar color/text-color/blur and derives its rounded corners from where it sits against
 //! the parent's edges ([`Paint::corner_style`] + the corners walk). Two text paths: the
 //! [`Paint::paint`] prim (carrying the configured statusbar font — the legacy default-font
-//! behavior on this path dropped the family and rendered sans), and pre-shaped glyphon
+//! behavior on this path dropped the family and rendered sans), and pre-shaped cosmic-text
 //! buffers through [`Paint::text_items`] (new with this migration) for manual hosts —
 //! cce-status-interface calls `prepare_text` then `get_text_items` into its own paint.
 
@@ -16,7 +16,7 @@ use crate::widget::{Adapted, Input, Layout, Paint};
 pub struct StatusBar {
     rect: Rect,
     pub text: String,
-    pub text_buf: Option<glyphon::Buffer>,
+    pub text_buf: Option<cosmic_text::Buffer>,
     pub text_offset_x: Option<f32>,
     pub text_color: Option<[f32; 4]>,
     pub bg_color: Option<[f32; 4]>,
@@ -178,7 +178,7 @@ impl Paint for StatusBar {
         Some(crate::layout::statusbar_font())
     }
 
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
+    fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
         if !self.text.is_empty() && self.text_buf.is_none() {
             let (font_fam, font_size) = crate::layout::statusbar_font_parsed();
             let size = if font_size > 0.0 { font_size } else { 12.0 };
@@ -205,7 +205,7 @@ mod tests {
     /// deleted; the bar's rendered text is the `Paint::paint` prim.)
     #[test]
     fn manual_host_text_pipeline() {
-        let mut fs = glyphon::FontSystem::new();
+        let mut fs = cosmic_text::FontSystem::new();
         // Pin the flat style: the bg-quad bridge under test is skipped by the
         // config-default recessed band.
         let mut bar = StatusBar::new().with_text("hello").with_text_offset_x(15.0).with_recess(false);
diff --git a/src/widget/display/text_label.rs b/src/widget/display/text_label.rs
index 8d60474..245c18d 100644
--- a/src/widget/display/text_label.rs
+++ b/src/widget/display/text_label.rs
@@ -74,6 +74,6 @@ impl TextLabel {
     }
 }
 
-pub(crate) fn make_widget_text_buffer(fs: &mut glyphon::FontSystem, text: &str, size: f32, font_family: &str) -> glyphon::Buffer {
+pub(crate) fn make_widget_text_buffer(fs: &mut cosmic_text::FontSystem, text: &str, size: f32, font_family: &str) -> cosmic_text::Buffer {
     crate::backend::window_runner::get_text_buffer(fs, text, size, Some(font_family))
 }
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 65f5e25..1de0af0 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1160,7 +1160,7 @@ impl Paint for TextBox {
 
     /// The legacy `prepare_text`: sync font family/size with the live config defaults, then
     /// shape the display text and record per-glyph advances (`map_x_to_idx` reads them).
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
+    fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
         let (style_family, style_size) = crate::layout::control_label_font_detached_parsed();
         if self.font_size == self.default_font_size {
             self.font_size = style_size;
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index be32ca0..aa5a99c 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -478,7 +478,7 @@ pub trait WidgetHost {
     fn focused(&self, ctx: &UiContext) -> bool {
         ctx.is_focused_id(self.base().id())
     }
-    fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem) {}
+    fn prepare_text(&mut self, _fs: &mut cosmic_text::FontSystem) {}
 
     fn set_visible(&mut self, _visible: bool) {}
     fn visible(&self) -> bool { true }
diff --git a/src/widget/model.rs b/src/widget/model.rs
index e057284..3cdf1fb 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -252,7 +252,7 @@ pub trait Paint {
     /// overrides). TextBox measures its glyph advances here — load-bearing for cursor↔pixel
     /// mapping, not just a render cache. Receives the laid-out content rect. Default: nothing
     /// to shape.
-    fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem, _rect: Rect) {}
+    fn prepare_text(&mut self, _fs: &mut cosmic_text::FontSystem, _rect: Rect) {}
 
     /// Whether [`paint`](Paint::paint) emits the widget's ENTIRE subtree, so the paint walk
     /// must not also descend into its (ctx-linked) children — the legacy
@@ -1101,7 +1101,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         Layout::register_embedded_children(&mut self.inner, host_id, ctx);
     }
 
-    fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+    fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem) {
         if self.visible() {
             let rect = self.content_rect();
             Paint::prepare_text(&mut self.inner, fs, rect);