From 1ff907f4cf06f9a6828334de73d663769b8f3ec9 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:06:50 +0800 Subject: [PATCH] feat(icons): draw the detail panel and chrome glyphs to one spec The detail panel mixed four stock lucide glyphs from four different weights and frames, and the title bar's "+" and "..." were the stock thin cross and three stroked r=1 rings sitting next to the app's own filled dock tiles. Side by side that read as glyphs from unrelated sets. Thirteen icons now share one spec: stroke 1.9, corner radius 2.6-3.2, a 3.2->20.8 optical span on both axes (circles widen a little, since a round shape reads smaller at equal geometry), and every node filled rather than stroked -- a stroked dot hazes over below 16px, which is where the stock ellipsis and list bullets fell apart. "+" is the one departure: a bare cross has no enclosing shape to carry weight, so it runs 2.4 on a tighter span. Two shapes changed metaphor rather than just proportion: Info drops the circled "i" -- the most-drawn icon there is, and one that says "help" as readily as "details" -- for a panel with two lines written in it, a picture of what the tab opens; Changes drops lucide's long arc slung between two floating rings for an actual commit graph, which also fixes the 11px branch mark on every sidebar row. The override is keyed on the asset *path*, and `icon_named!` derives `IconName` from those same filenames, so each arm redraws its glyph app-wide rather than only in the panel. Two consequences are handled here rather than left to discover: - `folder` and `file` are redrawn too. Overriding only `folder-open` would have split the file tree down the middle -- expanded rows on the new spec, collapsed rows and every file still stock. - the Settings nav's About row keeps its circled "i", now carried by its own `circle-info.svg`. There the glyph labels a section, and a document shape says nothing about *About*. Because the paths resolve on their own, the glyphs need no call-site changes: `Icon::new(IconName::Search)` already renders the new art. Call sites stay on the typed enum rather than stringly paths, which a rename would break silently. --- assets/icons/circle-info.svg | 1 + assets/icons/copy.svg | 1 + assets/icons/ellipsis.svg | 1 + assets/icons/eye.svg | 1 + assets/icons/file.svg | 1 + assets/icons/folder-closed.svg | 1 + assets/icons/folder-open.svg | 1 + assets/icons/folder.svg | 1 + assets/icons/git-branch.svg | 2 +- assets/icons/info.svg | 2 +- assets/icons/list.svg | 2 +- assets/icons/plus.svg | 1 + assets/icons/search.svg | 1 + src/ui/assets.rs | 77 ++++++++++++++++++++++++++++++---- src/ui/settings.rs | 35 +++++++++++----- 15 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 assets/icons/circle-info.svg create mode 100644 assets/icons/copy.svg create mode 100644 assets/icons/ellipsis.svg create mode 100644 assets/icons/eye.svg create mode 100644 assets/icons/file.svg create mode 100644 assets/icons/folder-closed.svg create mode 100644 assets/icons/folder-open.svg create mode 100644 assets/icons/folder.svg create mode 100644 assets/icons/plus.svg create mode 100644 assets/icons/search.svg diff --git a/assets/icons/circle-info.svg b/assets/icons/circle-info.svg new file mode 100644 index 00000000..ec30e25e --- /dev/null +++ b/assets/icons/circle-info.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 00000000..55af1b1f --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 00000000..2cbbc2be --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 00000000..6b0f0238 --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/file.svg b/assets/icons/file.svg new file mode 100644 index 00000000..fab17eac --- /dev/null +++ b/assets/icons/file.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/folder-closed.svg b/assets/icons/folder-closed.svg new file mode 100644 index 00000000..be3c5989 --- /dev/null +++ b/assets/icons/folder-closed.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/folder-open.svg b/assets/icons/folder-open.svg new file mode 100644 index 00000000..2c0a9527 --- /dev/null +++ b/assets/icons/folder-open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg new file mode 100644 index 00000000..be3c5989 --- /dev/null +++ b/assets/icons/folder.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/git-branch.svg b/assets/icons/git-branch.svg index b809d259..c85b0a16 100644 --- a/assets/icons/git-branch.svg +++ b/assets/icons/git-branch.svg @@ -1 +1 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index ec30e25e..2f5b329c 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list.svg b/assets/icons/list.svg index 981d0c33..f840375b 100644 --- a/assets/icons/list.svg +++ b/assets/icons/list.svg @@ -1 +1 @@ - + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 00000000..b9457a10 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 00000000..4fcdc073 --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1 @@ + diff --git a/src/ui/assets.rs b/src/ui/assets.rs index af8c1cd9..0122bb34 100644 --- a/src/ui/assets.rs +++ b/src/ui/assets.rs @@ -35,14 +35,28 @@ impl AssetSource for Assets { /// The bytes of a bundled agent brand mark, or `None` if `path` isn't one of /// ours. Kept as an explicit match (rather than `rust-embed`) because the set is /// tiny and fixed, and `include_bytes!` needs no extra build dependency. +/// +/// Note that matching on the *path* makes every arm here an app-wide override, +/// not a local one: `gpui_component_macros::icon_named!` derives `IconName` from +/// the downstream asset filenames, so `IconName::Search.path()` is literally +/// `"icons/search.svg"` and every `Icon::new(IconName::Search)` in the tree — +/// tty7's and gpui-component's own — resolves through the arm below. Adding a +/// name that upstream also ships redraws it everywhere; check the call sites +/// before doing so, and prefer a name upstream *doesn't* use (`circle-info`) +/// when only one place should change. fn agent_icon(path: &str) -> Option<&'static [u8]> { let bytes: &'static [u8] = match path { // Flush `>_` prompt glyph for the plain-shell tab avatar (Lucide's // unboxed `terminal`, which gpui-component doesn't bundle — it only // ships the boxed `square-terminal`). "icons/terminal.svg" => include_bytes!("../../assets/icons/terminal.svg"), - // Lucide's `git-branch`, for the sidebar row's branch line (gpui-component - // doesn't bundle a git glyph). + // A git glyph on the detail-panel spec below (gpui-component bundles + // none). Serves both the sidebar row's branch line and the Changes tab. + // Drawn as an actual commit graph — a trunk with a node at each end, + // branching once — rather than lucide's long arc slung between two + // floating rings: that one is the loosest, most lopsided shape in a row + // of four, and at the sidebar's 11px a stroked ring's hole collapses into + // a blur anyway. Nodes are filled here. "icons/git-branch.svg" => include_bytes!("../../assets/icons/git-branch.svg"), // Filled-panel dock glyphs for the window chrome: an outlined frame with // one inset solid block marking which dock is open (the "commercial-app" @@ -50,13 +64,62 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> { // tty7 carries its own filled variants for the title bar controls. "icons/panel-left.svg" => include_bytes!("../../assets/icons/panel-left.svg"), "icons/panel-right.svg" => include_bytes!("../../assets/icons/panel-right.svg"), - // Lucide's `list`, for the detail panel's Outline tab: the recorded - // commands are a plain list, not a terminal (`square-terminal` read as - // "this is a shell", which every pane already is). + // The chrome glyphs that sit beside those dock tiles, redrawn to share + // their weight and their solid-mass vocabulary. Lucide's stock `plus` is + // a thin bare cross that reads far lighter than the framed dock icon + // next to it, and its `ellipsis` strokes three `r=1` circles — at 18px + // the cap overlaps its own fill and the dots blur into grey smudges. + // + // `plus` is the one deliberate departure from the spec below: a bare + // cross has no enclosing shape to carry weight, so it runs stroke 2.4 on + // a tighter 6→18 span. Matching the spec exactly would leave it visibly + // frailer than the framed tile beside it. + "icons/plus.svg" => include_bytes!("../../assets/icons/plus.svg"), + "icons/ellipsis.svg" => include_bytes!("../../assets/icons/ellipsis.svg"), + // The detail panel's own set: four tab tiles at 18px and four controls at + // 13px, all in one panel, so they're drawn to one spec instead of taken + // from lucide as-is. + // + // stroke 1.9 (except `plus` — see above) + // radius 2.6–3.2, never 2.0 + // span 3.2→20.8, both axes; circles widen to ~2.5→21.5, since a + // round shape reads smaller at equal geometry + // nodes always filled, r ≥ 1.35 — a stroked dot hazes below 16px + // + // The stock glyphs share none of that: they mix stroke weights, sit a + // 21-wide circle next to an 18-wide folder, and leave so much dead space + // inside the frame that the row reads as four glyphs from four sets. + // + // Shape choices worth keeping: `info` is a panel with two lines written + // in it — a picture of what the tab actually opens (cwd, shell, branch, + // changes) — rather than the circled `i`, which is the most-drawn icon + // there is and says "help" as readily as "details"; Outline's last row is + // cut short, because three full-width rules read as a hamburger menu + // rather than a list; + // `folder-closed` drops its lid rule, which at 18px only crowded the top + // of the box; `copy`'s back sheet wraps three sides and stops on its own + // curves instead of poking two raw stubs out of an L. "icons/list.svg" => include_bytes!("../../assets/icons/list.svg"), - // A refined `info` glyph for the Info tab: a solid dot over a round-capped - // stem in a slightly inset circle, cleaner than the stock hairline `i`. + "icons/folder-closed.svg" => include_bytes!("../../assets/icons/folder-closed.svg"), + "icons/folder-open.svg" => include_bytes!("../../assets/icons/folder-open.svg"), "icons/info.svg" => include_bytes!("../../assets/icons/info.svg"), + "icons/eye.svg" => include_bytes!("../../assets/icons/eye.svg"), + "icons/search.svg" => include_bytes!("../../assets/icons/search.svg"), + "icons/copy.svg" => include_bytes!("../../assets/icons/copy.svg"), + // `folder` and `file` carry no detail-panel role of their own — they're + // here because overriding `folder-open` above would otherwise split the + // file tree down the middle, drawing expanded rows on this spec and + // collapsed ones (and every file) from stock lucide. Same shape as + // `folder-closed`: upstream's only difference between the two is the lid + // rule this set drops anyway. + "icons/folder.svg" => include_bytes!("../../assets/icons/folder.svg"), + "icons/file.svg" => include_bytes!("../../assets/icons/file.svg"), + // The circled `i` that `info.svg` used to be, kept under its own name for + // the Settings nav's About row — there the glyph labels a section rather + // than a detail tab, and "panel with two lines written in it" says nothing + // about *About*. No upstream `IconName` maps here, so it's referenced by + // path (see `settings.rs`). + "icons/circle-info.svg" => include_bytes!("../../assets/icons/circle-info.svg"), "icons/agents/claude.svg" => include_bytes!("../../assets/icons/agents/claude.svg"), "icons/agents/codex.svg" => include_bytes!("../../assets/icons/agents/codex.svg"), "icons/agents/gemini.svg" => include_bytes!("../../assets/icons/agents/gemini.svg"), diff --git a/src/ui/settings.rs b/src/ui/settings.rs index abc9de61..8ad3ffee 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -684,7 +684,7 @@ impl Tty7App { // active it also carries a trailing `(N)` count of that section's matching // settings — the full section nav stays put and is annotated with // per-section hit counts, rather than collapsing into a flat result list. - let nav_item = |label: &'static str, target: SettingsSection, icon: IconName| { + let nav_item = |label: &'static str, target: SettingsSection, icon: Icon| { let view = cx.entity(); let count = if query.is_empty() { 0 @@ -692,7 +692,7 @@ impl Tty7App { section_match_count(target, &query) }; let item = SidebarMenuItem::new(label) - .icon(Icon::new(icon)) + .icon(icon) .active(section == target) .on_click(move |_, _window, cx| { view.update(cx, |this, cx| this.select_settings_section(target, cx)); @@ -715,7 +715,7 @@ impl Tty7App { .child(nav_item( "Appearance", SettingsSection::Appearance, - IconName::Palette, + Icon::new(IconName::Palette), )) // Sliders for Terminal (it's the tuning page), the `>_` // prompt glyph for Shell (it configures the prompt's @@ -723,28 +723,43 @@ impl Tty7App { .child(nav_item( "Terminal", SettingsSection::Terminal, - IconName::Settings2, + Icon::new(IconName::Settings2), )) .child(nav_item( "Shell", SettingsSection::Shell, - IconName::SquareTerminal, + Icon::new(IconName::SquareTerminal), + )) + .child(nav_item( + "SSH", + SettingsSection::Ssh, + Icon::new(IconName::Globe), + )) + .child(nav_item( + "Agents", + SettingsSection::Agents, + Icon::new(IconName::Bot), )) - .child(nav_item("SSH", SettingsSection::Ssh, IconName::Globe)) - .child(nav_item("Agents", SettingsSection::Agents, IconName::Bot)) .child(nav_item( "Window & Tabs", SettingsSection::WindowTabs, - IconName::WindowRestore, + Icon::new(IconName::WindowRestore), )) // The icon set ships no keyboard glyph; CaseSensitive ("Aa") // is the closest key-ish cue available. .child(nav_item( "Keybindings", SettingsSection::Keybindings, - IconName::CaseSensitive, + Icon::new(IconName::CaseSensitive), )) - .child(nav_item("About", SettingsSection::About, IconName::Info)); + // Not `IconName::Info`: `icons/info.svg` is overridden app-wide with + // the detail panel's "panel with two lines" glyph, which reads as a + // document, not as *About*. This row keeps the circled `i`. + .child(nav_item( + "About", + SettingsSection::About, + Icon::empty().path("icons/circle-info.svg"), + )); let sidebar = Sidebar::new("settings-sidebar") .collapsible(SidebarCollapsible::None)