git.lucas.co / cce-grid
desktop grid client
git clone https://git.lucas.co/cce-grid.git

commita271fcb20ce9dde15a91ea20b18d3b672f0e393b
parent182984ad88
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 22:08
fix: a link card leads its URL with the link glyph, not an emoji

The card's text was "🔗 {url}", an emoji in whatever face the font fell
back to. The URL is laid out alone, a glyph's room narrower, and the card
paints the cce-icons link glyph before it, in the URL's colour on its
first line — as cce-ui's markdown leads an embed it cannot show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md   |  5 ++++-
 src/main.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 6447c8c..4905557 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -71,7 +71,10 @@ no vault, `$XDG_DATA_HOME/cce/desktop.canvas`. Obsidian-on-cce milestone 5.
 - **Cards** (note, text, link, file) draw through cce-ui's `MarkdownView`
   (`widget::markdown`, the `markdown` feature): laid out in virtual units
   (`layout_cards`, cached per node/width/text), painted with
-  `paint_scaled(k = patch.scale)`. Two traps, both fixed here:
+  `paint_scaled(k = patch.scale)`. A link card lays out the URL alone,
+  `link_glyph_room` narrower, and `paint_card` leads it with the `link`
+  cce-icons glyph (it was a "🔗 " emoji in the text until 2026-10-05).
+  Two traps, both fixed here:
   - **The scale factor.** The runner renders this surface at scale 1 but
     the toolkit-wide factor follows the output (2 on the laptop), and text
     shapes at it: `display_list` pins it to the surface's own scale, or
diff --git a/src/main.rs b/src/main.rs
index ca07526..5d1dc8a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -741,7 +741,8 @@ impl GridApp {
         let s = p.scale;
         let mut jobs: Vec<(String, String, f64)> = Vec::new();
         for (item, _) in &self.items {
-            let width = (item.w - 2.0 * CARD_PAD).max(20.0);
+            let lead = if matches!(item.kind, Kind::Link(_)) { link_glyph_room() as f64 } else { 0.0 };
+            let width = (item.w - 2.0 * CARD_PAD - lead).max(20.0);
             let source = match &item.kind {
                 Kind::Note(path) => {
                     if self.cards.get(&item.node).is_some_and(|c| (c.width - width).abs() < 0.5) {
@@ -750,7 +751,8 @@ impl GridApp {
                     std::fs::read_to_string(path).unwrap_or_else(|_| format!("*{} is missing*", item.name()))
                 }
                 Kind::Text(t) => t.clone(),
-                Kind::Link(u) => format!("🔗 {u}"),
+                // The URL alone; `paint_card` leads it with the `link` glyph.
+                Kind::Link(u) => u.clone(),
                 Kind::File(_) => format!("**{}**", item.name()),
                 _ => continue,
             };
@@ -796,8 +798,41 @@ impl GridApp {
         if let Some(c) = self.cards.get(&item.node) {
             let pad = (CARD_PAD * s) as f32;
             let inner = Rect { x: r.x + pad * 0.5, y: r.y + pad * 0.5, width: r.width - pad, height: r.height - pad };
+            // A link card's URL is led by the `link` glyph, in the colour
+            // the URL is drawn in, centred on its first line — the way
+            // cce-ui's markdown leads an embed it cannot show inline. The
+            // URL was laid out `link_glyph_room` narrower to make the room.
+            let glyph = matches!(item.kind, Kind::Link(_)).then(|| {
+                let (size, side) = (CARD_TEXT, link_glyph_side());
+                let (y, color) = c
+                    .layout
+                    .draws
+                    .iter()
+                    .find_map(|d| match d {
+                        markdown::Draw::Text { y, color, .. } => Some((*y, *color)),
+                        _ => None,
+                    })
+                    .unwrap_or((0.0, cce_ui::colors::TEXT_FG));
+                // A text draw's colour is linear (it is painted through
+                // `srgb_u8`); a glyph is tinted in sRGB, as a text colour.
+                let color = cce_ui::colors::to_srgb(color);
+                let sf = s as f32;
+                let rect = Rect {
+                    x: r.x + pad,
+                    y: r.y + pad + (y + 0.5 * (size * 1.3 - side)) * sf,
+                    width: side * sf,
+                    height: side * sf,
+                };
+                (rect, color)
+            });
+            let lead = if glyph.is_some() { link_glyph_room() * s as f32 } else { 0.0 };
             for frag in uncovered(inner, above) {
-                pc.clip(frag, |pc| c.layout.paint_scaled(pc, (r.x + pad, r.y + pad), s as f32, frag));
+                pc.clip(frag, |pc| {
+                    if let Some((rect, color)) = glyph {
+                        pc.icon("link", rect, color);
+                    }
+                    c.layout.paint_scaled(pc, (r.x + pad + lead, r.y + pad), s as f32, frag)
+                });
             }
         }
     }
@@ -912,6 +947,17 @@ fn anchor(i: &items::DesktopItem, side: Option<&str>, toward: (f64, f64)) -> ((f
     }
 }
 
+/// The `link` glyph's side on a link card, in card units (the size cce-ui's
+/// markdown gives the glyph leading an embed).
+fn link_glyph_side() -> f32 {
+    (CARD_TEXT * 0.85).round()
+}
+
+/// What a link card's URL is moved right by: the glyph and a gap.
+fn link_glyph_room() -> f32 {
+    link_glyph_side() + (CARD_TEXT * 0.4).round()
+}
+
 fn srgb_u8(linear: [f32; 4]) -> [u8; 3] {
     let c = cce_ui::colors::to_srgb(linear);
     [(c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8]