feat(ui): carry the v4 chrome into panes, the file viewer and SFTP

- theme: additive helpers for a device-pixel hairline, tabular figures
  and an inverted neutral button variant.
- Pane splits rest as a device-pixel hairline in the divider tone; hover
  and drag keep the accent at 1px like the other resize edges.
- File viewer header: medium file name, 5px unsaved dot, 26px/6px close
  tile with its glyph on the content inset, divider hairline under it.
  Status bar: divider hairline, caption size, tabular line/column.
- SFTP browser: file-tree rows (26px, 6px corner, 16px caption glyphs),
  breadcrumb and notes on the 20px text column, a borderless edit well,
  inverted OK button, and ink-on-track transfer progress.
- Forward rows line up with the process and port rows (text at 20px,
  6px corner); Add and Reconnect use the inverted neutral fill.
This commit is contained in:
l0ng-ai
2026-09-24 20:43:51 +08:00
parent 6fb4bb2fe7
commit 39e266b3aa
5 changed files with 209 additions and 89 deletions
+39 -17
View File
@@ -1360,32 +1360,44 @@ impl Tty7App {
row
};
let menu_app = cx.entity().downgrade();
// v4 chrome: the file name in body ink at medium — the one heading the
// column has — a hairline in the divider tone under the bar, and the
// rail's 26px close tile, so the header reads as part of the plane it
// sits in rather than a toolbar bolted on top of it.
let (tile, glyph) = (
crate::ui::tab_strip::RAIL_TILE,
crate::ui::tab_strip::RAIL_TILE_GLYPH,
);
row.flex_none()
.h(px(crate::ui::app::TITLE_BAR_HEIGHT))
.items_center()
.gap_1p5()
.gap(px(8.))
.pl(px(lead))
.pr(px(crate::ui::app::tile_trailing_inset()))
.border_b_1()
.border_color(cx.theme().border)
// The glyph, not the tile, lands on `CONTENT_INSET`, the column
// the file name starts on at the other end of the bar.
.pr(px(crate::ui::app::CONTENT_INSET - (tile - glyph) / 2.))
.border_b(crate::ui::theme::hairline(window))
.border_color(cx.theme().sidebar_border)
.child(
div()
.flex_1()
.min_w_0()
.text_ellipsis()
.text_sm()
.when(name.is_none(), |d| {
d.text_color(cx.theme().muted_foreground)
.text_size(gpui::rems(crate::ui::right_panel::TEXT))
.map(|d| match name.is_some() {
true => d.font_weight(gpui::FontWeight::MEDIUM),
false => d.text_color(cx.theme().muted_foreground),
})
.child(
name.unwrap_or_else(|| SharedString::from(t(L10nKey::EditorNoFileOpen))),
),
)
// Unsaved: the sidebar's 5px status dot, in the warning ink.
.when(dirty, |d| {
d.child(
div()
.flex_none()
.size(px(6.))
.size(px(crate::ui::tab_strip::ROW_STATUS_DOT))
.rounded_full()
.bg(cx.theme().warning),
)
@@ -1394,12 +1406,12 @@ impl Tty7App {
div().occlude().flex_shrink_0().child(
crate::ui::tab_strip::chrome_tile_sized(
Button::new("editor-panel-close").icon(Icon::new(IconName::Close)),
crate::ui::app::TILE_SIZE,
crate::ui::app::TILE_GLYPH_LINE,
tile,
glyph,
false,
cx,
)
.rounded_lg()
.rounded(px(crate::ui::tab_strip::RAIL_TILE_RADIUS))
.tooltip(t(L10nKey::EditorBackToTerminal))
.on_click(cx.listener(|this, _, window, cx| {
this.toggle_code_panel(window, cx);
@@ -1411,7 +1423,7 @@ impl Tty7App {
})
}
fn render_code_status_bar(&self, _window: &Window, cx: &mut Context<Self>) -> gpui::Div {
fn render_code_status_bar(&self, window: &Window, cx: &mut Context<Self>) -> gpui::Div {
// The roots below belong to this window's own machine. A file read
// over SFTP is on another one, where they mean nothing, so it shows
// its own full path rather than borrowing the local repo's name.
@@ -1455,16 +1467,18 @@ impl Tty7App {
let is_markdown = active.is_some_and(|f| language_for_path(&f.path) == "markdown");
let preview = active.is_some_and(|f| f.preview);
// Metadata, not a toolbar: caption ink on the plane's own fill, set
// off by a hairline in the divider tone rather than a control border.
h_flex()
.flex_none()
.w_full()
.h(px(26.))
.items_center()
.gap_3()
.px_3()
.border_t_1()
.border_color(cx.theme().border)
.text_xs()
.px(px(crate::ui::app::CONTENT_INSET))
.border_t(crate::ui::theme::hairline(window))
.border_color(cx.theme().sidebar_border)
.text_size(gpui::rems(crate::ui::right_panel::META))
.text_color(muted)
.when_some(path_text, |this, t| {
this.child(div().min_w_0().text_ellipsis().child(t))
@@ -1500,7 +1514,15 @@ impl Tty7App {
),
)
})
.when_some(cursor, |this, t| this.child(div().child(t)))
// Tabular figures, so the position does not jitter sideways as
// the caret walks from line 9 to line 10.
.when_some(cursor, |this, t| {
this.child(
div()
.font_features(crate::ui::theme::tabular_figures())
.child(t),
)
})
}
fn render_editor_empty(&self, cx: &Context<Self>) -> gpui::Div {
+23 -8
View File
@@ -5,7 +5,7 @@ use gpui_component::{ActiveTheme as _, Disableable as _, IconName, Sizable as _,
use crate::daemon::protocol::{ForwardStatus, ManagedForward, SshForwardKind, SshForwardRule};
use crate::terminal::view::TerminalView;
use crate::ui::app::{CONTENT_INSET, Tty7App};
use crate::ui::app::Tty7App;
use crate::ui::i18n::{L10nKey, t, t_fmt};
use crate::ui::right_panel::{META, TEXT_MONO};
@@ -179,6 +179,7 @@ impl Tty7App {
let theme = cx.theme();
let (danger, foreground) = (theme.danger, theme.foreground);
let theme_popover = theme.popover;
let bar = crate::ui::notice::pill(danger, cx)
.child(
@@ -217,9 +218,12 @@ impl Tty7App {
.map(|keys| div().child(format!("· {keys}"))),
)
.child(
// The strip's one action, as v4's primary: inverted neutral on
// the popover fill the pill floats on. The red is the edge
// and the reason; the button does not repeat it.
Button::new("ssh-reconnect")
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reconnect))
.primary()
.custom(crate::ui::theme::inverted_button(theme_popover, cx))
.small()
.on_click(
cx.listener(|this, _, window, cx| this.restart_ssh_session(window, cx)),
@@ -277,9 +281,14 @@ impl Tty7App {
.group(group.clone())
.items_center()
.gap(px(8.))
.px(px(4.))
.py(px(5.))
.rounded(crate::ui::rounding::ROW_RADIUS)
// The port and process rows' box: 26px, padded `ROW_INSET` so the
// text lands on the panel's 20px column and the trailing tile ends
// 20px from the edge, on the panel's row corner. A described
// forward grows by its second line instead of being clipped to one.
.min_h(px(26.))
.py(px(3.))
.px(px(crate::ui::right_panel::ROW_INSET))
.rounded(crate::ui::right_panel::ROW_FILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(gpui::rgb(sf.hover)))
.on_click(cx.listener(move |this, _, window, cx| {
@@ -342,7 +351,7 @@ impl Tty7App {
)
.w(px(crate::ui::tab_strip::MIN_TARGET))
.h(px(crate::ui::tab_strip::MIN_TARGET))
.rounded(px(4.))
.rounded(px(crate::ui::tab_strip::RAIL_TILE_RADIUS))
.tooltip(t(L10nKey::ForwardTooltipRemove))
.on_click(cx.listener(
move |this, _, _window, cx| {
@@ -401,8 +410,9 @@ impl Tty7App {
.child(div().w(px(52.)).child(Input::new(port).xsmall()))
};
// Form text, not rows: on the panel's text column.
v_flex()
.px(px(CONTENT_INSET))
.px(px(crate::ui::right_panel::TEXT_INSET))
.pt(px(6.))
.pb(px(2.))
.gap(px(5.))
@@ -547,7 +557,12 @@ impl Tty7App {
} else {
t(L10nKey::ForwardAdd)
})
.primary()
// v4's primary: the panel's ink as the
// fill, its opaque surface as the label.
.custom(crate::ui::theme::inverted_button(
gpui::rgb(sf.base).into(),
cx,
))
.xsmall()
.disabled(!complete)
.on_click(cx.listener(move |this, _, window, cx| {
+16 -4
View File
@@ -1080,8 +1080,14 @@ impl Pane<PaneSlot> {
let row = *axis == Axis::Horizontal;
let r = ratio.get().clamp(MIN_RATIO, MAX_RATIO);
let idle = cx.theme().border;
// At rest the split is a v4 hairline in the divider tone — the
// line between two panes that already carry their own content,
// not the stronger rule that outlines a floating surface. Hover
// and drag keep the accent the sidebar and right-panel edges
// light up with, so every resize handle answers the same way.
let idle = cx.theme().sidebar_border;
let active = cx.theme().drag_border;
let hairline = crate::ui::theme::hairline(window);
let container: Rc<Cell<Option<Bounds<Pixels>>>> = Rc::new(Cell::new(None));
@@ -1141,6 +1147,9 @@ impl Pane<PaneSlot> {
.size_full();
let line_color = if dragging.get() { active } else { idle };
// A lit handle goes back to a whole pixel: a hairline of accent
// is too faint to say "this is what you are holding".
let line = if dragging.get() { px(1.) } else { hairline };
// The gutter stays 5px so the split looks the same; the target
// is the 8px the sidebar and right-panel edges already hand
// you. The extra 1.5px a side reaches into the pane's own 8px
@@ -1182,10 +1191,13 @@ impl Pane<PaneSlot> {
.when(!row, |d| d.h(px(DIVIDER_THICKNESS)).w_full())
.child(
div()
.when(row, |d| d.w(px(1.)).h_full())
.when(!row, |d| d.h(px(1.)).w_full())
.when(row, |d| d.w(line).h_full())
.when(!row, |d| d.h(line).w_full())
.bg(line_color)
.group_hover("split-divider", |s| s.bg(active)),
.group_hover("split-divider", move |s| match row {
true => s.bg(active).w(px(1.)),
false => s.bg(active).h(px(1.)),
}),
)
.child(grab);
+101 -60
View File
@@ -22,7 +22,20 @@ use crate::daemon::ssh::sftp::{remote_basename, remote_join, remote_parent, safe
use crate::terminal::RemoteTerminal;
use crate::ui::app::{CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App};
use crate::ui::i18n::{L10nKey, t, t_fmt};
use crate::ui::right_panel::{META, TEXT};
use crate::ui::right_panel::{
HEADING, META, ROW_FILL_RADIUS, ROW_GLYPH, ROW_INSET, TEXT, TEXT_INSET,
};
/// A remote directory row: the local file tree's 26px row, on the panel's
/// `ROW_FILL_RADIUS`, so the Files tab keeps its rhythm whichever machine it
/// is showing.
const SFTP_ROW_H: f32 = 26.;
/// The gap between a row's icon, name and size — the file tree's.
const SFTP_ROW_GAP: f32 = 6.;
/// The transfer tray's disclosure chevron — the file tree's.
const TRAY_CHEVRON: f32 = 10.;
#[derive(Clone, Copy)]
enum SftpMenuAction {
@@ -1357,7 +1370,8 @@ impl Tty7App {
.flex_wrap()
.items_center()
.gap_0p5()
.px(px(CONTENT_INSET))
// Text, not a row: on the panel's text column.
.px(px(TEXT_INSET))
.pb(px(4.))
.on_double_click(
cx.listener(|this, _, window, cx| this.sftp_begin_edit_path(window, cx)),
@@ -1395,9 +1409,10 @@ impl Tty7App {
}
fn render_sftp_edit_form(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
let secondary = cx.theme().secondary;
let border = cx.theme().border;
let well = cx.theme().muted;
let foreground = cx.theme().foreground;
let surface: gpui::Hsla =
gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().sidebar.base).into();
let (title, input): (String, _) = match self.sftp_panel.editing.as_ref()? {
SftpEdit::NewFolder(input) => (t(L10nKey::SftpEditNewFolder).to_string(), input),
SftpEdit::NewFile(input) => (t(L10nKey::SftpEditNewFile).to_string(), input),
@@ -1416,14 +1431,16 @@ impl Tty7App {
Some(
v_flex()
.id("panel-sftp-edit")
.gap(px(5.))
.mx(px(CONTENT_INSET - 4.))
.gap(px(6.))
// Where a row's fill would be, its text on the row's text
// column.
.mx(px(CONTENT_INSET))
.mb(px(4.))
.p(px(6.))
.bg(secondary)
.border_1()
.border_color(border)
.rounded_md()
.p(px(ROW_INSET))
// A borderless faint well, the file tree's search shape, not
// an outlined card: it is part of the list, not over it.
.bg(well)
.rounded(px(7.))
// Escape backs out of the form, the way it backs out of the
// path editor above it and every sheet the app puts up.
.on_key_down(cx.listener(|this, ev: &gpui::KeyDownEvent, window, cx| {
@@ -1433,7 +1450,7 @@ impl Tty7App {
}))
.child(
div()
.text_xs()
.text_size(rems(HEADING))
.font_weight(FontWeight::MEDIUM)
.text_color(foreground)
.child(title),
@@ -1456,7 +1473,7 @@ impl Tty7App {
Button::new("sftp-edit-ok")
.label(t(L10nKey::Ok))
.xsmall()
.primary()
.custom(crate::ui::theme::inverted_button(surface, cx))
.disabled(!can_commit)
.on_click(
cx.listener(|this, _, w, cx| this.sftp_commit_edit(w, cx)),
@@ -1475,12 +1492,12 @@ impl Tty7App {
.min_h_0()
.overflow_y_scroll()
.track_scroll(&self.sftp_panel.scroll)
.px(px(CONTENT_INSET - 6.))
.px(px(CONTENT_INSET))
.pb(px(4.));
let note = |text: gpui::SharedString, color| {
div()
.px(px(6.))
.px(px(ROW_INSET))
.py(px(4.))
.text_size(rems(TEXT))
.text_color(color)
@@ -1521,33 +1538,41 @@ impl Tty7App {
}
fn render_sftp_go_up_row(&self, cx: &mut Context<Self>) -> AnyElement {
let foreground = cx.theme().foreground;
let theme = cx.theme();
let (name_ink, muted) = (theme.sidebar_foreground, theme.muted_foreground);
let sf = cx.global::<crate::ui::presets::Surfaces>().popover;
h_flex()
.id("sftp-go-up")
.items_center()
.gap_1()
.pl(px(6.))
.pr_1()
.py_1()
.rounded(cx.theme().radius)
.gap(px(SFTP_ROW_GAP))
.h(px(SFTP_ROW_H))
.px(px(ROW_INSET))
.rounded(ROW_FILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(gpui::rgb(sf.hover)))
.child(
Icon::new(IconName::FolderOpen)
.xsmall()
.text_color(foreground),
.size(px(ROW_GLYPH))
.text_color(muted),
)
.child(
div()
.flex_1()
.min_w_0()
.text_size(rems(TEXT))
.text_color(name_ink)
.child(".."),
)
.child(div().flex_1().min_w_0().text_sm().child(".."))
.on_click(cx.listener(|this, _, _w, cx| this.sftp_up(cx)))
.into_any_element()
}
fn render_sftp_row(&self, entry: &SftpEntry, cx: &mut Context<Self>) -> AnyElement {
let foreground = cx.theme().foreground;
// The file tree's row: every glyph in caption ink, the name in the
// rail's title ink, the size a tabular caption at the trailing end.
let name_ink = cx.theme().sidebar_foreground;
let muted = cx.theme().muted_foreground;
let dir_color = foreground;
let list_hover = cx.theme().list_hover;
let list_hover = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().popover.hover);
let entry = entry.clone();
let dir_like = is_dir_like(&entry);
let icon = if dir_like {
@@ -1575,13 +1600,12 @@ impl Tty7App {
h_flex()
.id(row_id)
.items_center()
.gap_1()
.pl(px(6.))
.pr_1()
.py_1()
.rounded(cx.theme().radius)
.gap(px(SFTP_ROW_GAP))
.h(px(SFTP_ROW_H))
.px(px(ROW_INSET))
.rounded(ROW_FILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(list_hover))
.hover(move |s| s.bg(list_hover))
// Single click, the same gesture the local file tree answers —
// this panel used to demand a double click because its open
// action was a download, and that caution outlived the download.
@@ -1591,21 +1615,24 @@ impl Tty7App {
this.sftp_open_entry(open_entry.clone(), window, cx)
}),
)
.child(
Icon::new(icon)
.xsmall()
.text_color(if dir_like { dir_color } else { muted }),
)
.child(Icon::new(icon).size(px(ROW_GLYPH)).text_color(muted))
.child(
div()
.flex_1()
.min_w_0()
.text_sm()
.text_color(foreground)
.text_size(rems(TEXT))
.text_color(name_ink)
.truncate()
.child(name_label),
)
.child(div().flex_none().text_xs().text_color(muted).child(size))
.child(
div()
.flex_none()
.text_size(rems(META))
.font_features(crate::ui::theme::tabular_figures())
.text_color(muted)
.child(size),
)
.context_menu(move |menu, _window, cx| {
let danger = cx.theme().danger;
Self::sftp_row_context_menu(menu, &menu_entry, dir_like, is_symlink, danger, &app)
@@ -1716,8 +1743,10 @@ impl Tty7App {
let muted = cx.theme().muted_foreground;
let danger = cx.theme().danger;
let accent = cx.theme().accent;
let border = cx.theme().border;
// Progress in v4 is ink on a faint track, not the accent: the bar is a
// reading, and blue is kept for the things you can press.
let (ink, track) = (cx.theme().foreground, cx.theme().muted);
let divider = cx.theme().sidebar_border;
let hover = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().sidebar.hover);
let expanded = self.sftp_panel.tray_expanded || history;
@@ -1769,16 +1798,21 @@ impl Tty7App {
.id("sftp-transfers-summary")
.items_center()
.gap(px(6.))
.px(px(CONTENT_INSET))
// The disclosure hangs in the gutter so the summary's text starts
// on the panel's text column, the way a tree row's chevron does.
.pl(px(TEXT_INSET - TRAY_CHEVRON - 6.))
.pr(px(CONTENT_INSET))
.h(px(28.))
.cursor_pointer()
.hover(move |s| s.bg(hover))
.on_click(cx.listener(|this, _, _w, cx| this.sftp_toggle_tray(cx)))
.child(
div()
.text_size(rems(META))
.text_color(muted)
.child(if expanded { "⌄" } else { "›" }),
Icon::new(match expanded {
true => IconName::ChevronDown,
false => IconName::ChevronRight,
})
.size(px(TRAY_CHEVRON))
.text_color(muted),
)
.child(
div()
@@ -1786,6 +1820,7 @@ impl Tty7App {
.min_w_0()
.truncate()
.text_size(rems(META))
.font_features(crate::ui::theme::tabular_figures())
.text_color(summary_color)
.child(summary),
)
@@ -1803,17 +1838,17 @@ impl Tty7App {
)
.w(px(crate::ui::tab_strip::MIN_TARGET))
.h(px(crate::ui::tab_strip::MIN_TARGET))
.rounded(px(4.))
.rounded(px(crate::ui::tab_strip::RAIL_TILE_RADIUS))
.tooltip(t(L10nKey::Dismiss))
.on_click(cx.listener(|this, _, _w, cx| this.sftp_dismiss_tray(cx))),
),
);
let underline = div().h(px(2.)).w_full().bg(border).child(
let underline = div().h(px(2.)).w_full().bg(track).child(
div()
.h_full()
.w(gpui::relative((pct / 100.0) as f32))
.bg(if failed > 0 { danger } else { accent }),
.bg(if failed > 0 { danger } else { ink }),
);
let body = expanded.then(|| {
@@ -1830,14 +1865,14 @@ impl Tty7App {
};
v_flex().child(
div()
.px(px(CONTENT_INSET))
.px(px(TEXT_INSET))
.py(px(3.))
.text_size(rems(META))
.text_color(color)
.child(text),
)
} else {
let mut list = v_flex().px(px(CONTENT_INSET)).pb(px(6.)).gap(px(6.));
let mut list = v_flex().px(px(TEXT_INSET)).pb(px(8.)).gap(px(8.));
for job in jobs {
list = list.child(self.render_sftp_job(job, cx));
}
@@ -1866,7 +1901,7 @@ impl Tty7App {
v_flex()
.flex_none()
.border_t_1()
.border_color(border)
.border_color(divider)
.child(head)
.when(running > 0 && !expanded, |this| this.child(underline))
.children(body)
@@ -1876,11 +1911,10 @@ impl Tty7App {
fn render_sftp_job(&self, job: &SftpJobProgress, cx: &mut Context<Self>) -> Div {
let foreground = cx.theme().foreground;
let border = cx.theme().border;
let track = cx.theme().muted;
let danger = cx.theme().danger;
let success = cx.theme().success;
let muted = cx.theme().muted_foreground;
let accent = cx.theme().accent;
let arrow = match job.kind {
SftpTransferKind::Upload => "↑",
SftpTransferKind::Download => "↓",
@@ -1912,10 +1946,11 @@ impl Tty7App {
SftpJobState::Done => success,
_ => muted,
};
// The tray's own bar: body ink on the faint track, red once it fails.
let bar_color = if matches!(job.state, SftpJobState::Error) {
danger
} else {
accent
foreground
};
let job_id = job.job_id;
let running = matches!(job.state, SftpJobState::Running);
@@ -1934,7 +1969,7 @@ impl Tty7App {
div()
.flex_1()
.min_w_0()
.text_xs()
.text_size(rems(META))
.text_color(foreground)
.truncate()
.child(format!("{arrow} {name}")),
@@ -1971,7 +2006,7 @@ impl Tty7App {
}),
)
.child(
div().h(px(3.)).w_full().rounded_full().bg(border).child(
div().h(px(3.)).w_full().rounded_full().bg(track).child(
div()
.h_full()
.w(gpui::relative((pct / 100.0) as f32))
@@ -1979,7 +2014,13 @@ impl Tty7App {
.bg(bar_color),
),
)
.child(div().text_xs().text_color(status_color).child(status))
.child(
div()
.text_size(rems(META))
.font_features(crate::ui::theme::tabular_figures())
.text_color(status_color)
.child(status),
)
}
}
+30
View File
@@ -947,6 +947,36 @@ pub(crate) fn floating_surface<T: Styled>(element: T, cx: &App) -> T {
.shadow_xl()
}
/// One device pixel: the v4 hairline, 0.5px on a Retina panel and a whole
/// pixel on a 1x one. A literal `px(0.5)` would be antialiased down to half
/// its ink on a 1x display, under the contrast floor `divider` is held to.
pub(crate) fn hairline(window: &Window) -> Pixels {
px(1. / window.scale_factor().max(1.))
}
/// Tabular figures, so a column of counts, sizes and positions keeps its
/// digits on one grid while the numbers change under it.
pub(crate) fn tabular_figures() -> gpui::FontFeatures {
gpui::FontFeatures(std::sync::Arc::new(vec![("tnum".to_string(), 1)]))
}
/// The v4 primary button: an inverted neutral — body ink as the fill, the
/// surface it sits on as the label — rather than the accent, which stays with
/// focus and the things that are on. `surface` is the opaque fill under the
/// button, so the label reads as cut out of it. Disabled, gpui-component
/// drops the fill to 15% of itself, the faint well the rest of v4 uses.
pub(crate) fn inverted_button(
surface: Hsla,
cx: &App,
) -> gpui_component::button::ButtonCustomVariant {
let ink = cx.theme().foreground;
gpui_component::button::ButtonCustomVariant::new(cx)
.color(ink)
.foreground(surface)
.hover(ink.blend(surface.opacity(0.14)))
.active(ink.blend(surface.opacity(0.24)))
}
/// On-state shares the accent role with sliders and primary actions.
pub(crate) fn switch(id: impl Into<gpui::ElementId>, cx: &App) -> gpui_component::switch::Switch {
let accent = cx.global::<presets::ActiveAccent>().0;