//! The Appearance page, laid out like T3 Code's: Color scheme tiles
//! (System, Light, Dark), a card per theme family where each swatch makes
//! that theme the light or the dark one, then the Interface rows.
use gpui_kit::component::{Icon, IconName, h_flex, tooltip::Tooltip, v_flex};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use super::SettingsView;
use super::widgets::{Section, SettingRow, choice, choice_of, reset, switch};
use crate::settings::{self, ChatWidth, DiffColors, ThemeMode};
use crate::ui::metal::{self, MetalStyled as _, Palette, vertical};
use crate::ui::themes::{self, FAMILIES, Theme};
const CHAT_WIDTHS: &[(ChatWidth, &str)] =
&[(ChatWidth::Comfortable, "Comfortable (default)"), (ChatWidth::Wide, "Wide"), (ChatWidth::Full, "Full")];
impl SettingsView {
pub(super) fn render_appearance(&mut self, cx: &mut Context<Self>) -> Vec<AnyElement> {
let p = metal::palette(cx);
let saved = settings::load();
let view = cx.weak_entity();
let colors = v_flex()
.gap(px(24.))
.child(
v_flex()
.gap(px(12.))
.child(label("Color scheme", p))
.child(div().id("color-scheme").child(scheme_tiles(saved.theme_mode, &view, cx))),
)
.child(
v_flex()
.gap(px(12.))
.child(h_flex().min_h(px(32.)).child(label("Themes", p)))
.child(theme_cards(&saved.light_theme, &saved.dark_theme, &view, cx)),
);
let colors = Section::new("Colors & themes").id("appearance").row(colors).hide_title().render(cx);
let ink = metal::diff_ink(p);
let diff_dots = h_flex()
.gap(px(3.))
.child(div().size(px(8.)).rounded_full().bg(ink.removed))
.child(div().size(px(8.)).rounded_full().bg(ink.added));
let interface = Section::new("Interface")
.id("appearance-interface")
.row(
SettingRow::new("Diff colors")
.id("diff-color-scheme")
.description("Choose colors for additions and deletions, including change counts.")
.reset(reset(&view, cx, |s| s.diff_colors, |s, colors| s.diff_colors = colors))
.control(h_flex().gap(px(8.)).child(diff_dots).child(choice_of(
"diff-colors",
saved.diff_colors,
vec![
(DiffColors::RedGreen, "Red & green (default)".into()),
(DiffColors::BlueOrange, "Blue & orange".into()),
],
Some(match saved.diff_colors {
DiffColors::RedGreen => "Red & green".into(),
DiffColors::BlueOrange => "Blue & orange".into(),
}),
&view,
|settings, colors| settings.diff_colors = colors,
))),
)
.row(
SettingRow::new("Composer context")
.id("composer-context")
.description("Keep branch and worktree controls below the composer after a thread starts.")
.reset(reset(&view, cx, |s| s.composer_context, |s, on| s.composer_context = on))
.control(switch("composer-context", saved.composer_context, &view, p, |settings, on| {
settings.composer_context = on
})),
)
.row(
SettingRow::new("Chat width")
.id("chat-width")
.description("Set how wide messages and the composer can grow on large screens.")
.reset(reset(&view, cx, |s| s.chat_width, |s, width| s.chat_width = width))
.control(choice("chat-width", saved.chat_width, CHAT_WIDTHS, &view, |settings, width| {
settings.chat_width = width
})),
)
.row(
SettingRow::new("Thread cards")
.id("thread-cards")
.description("Comfortable cards show the project, branch and agent. Compact ones fit a thread on one line.")
.reset(reset(&view, cx, |s| s.compact_threads, |s, compact| s.compact_threads = compact))
.control(choice("thread-density", saved.compact_threads, &[(false, "Comfortable"), (true, "Compact")], &view, {
|settings, compact| settings.compact_threads = compact
})),
)
.render(cx);
vec![colors, interface]
}
}
/// A label over a group on the page, like T3 Code's `h3`s.
fn label(text: &'static str, p: &Palette) -> Div {
div().px(px(16.)).text_size(px(14.)).text_color(p.ink2).child(text)
}
/// System, Light and Dark, each a miniature of the app in that mode.
fn scheme_tiles(mode: ThemeMode, view: &WeakEntity<SettingsView>, cx: &App) -> Div {
let p = metal::palette(cx);
let (light_id, dark_id) = settings::get(|settings| (settings.light_theme.clone(), settings.dark_theme.clone()));
let light = themes::find(&light_id, false);
let dark = themes::find(&dark_id, true);
let tiles = [
(ThemeMode::System, "System", "Follow the system appearance"),
(ThemeMode::Light, "Light", "Use light mode"),
(ThemeMode::Dark, "Dark", "Use dark mode"),
];
div().grid().grid_cols(3).gap(px(12.)).children(tiles.into_iter().map(|(value, name, tooltip)| {
let selected = value == mode;
let view = view.clone();
let preview = match value {
ThemeMode::Light => themes::preview(light).into_any_element(),
ThemeMode::Dark => themes::preview(dark).into_any_element(),
// Light on the left, dark on the right.
ThemeMode::System => h_flex()
.size_full()
.rounded(px(14.))
.overflow_hidden()
.child(div().w_1_2().h_full().child(themes::preview(light).rounded_r(px(0.))))
.child(div().w_1_2().h_full().child(themes::preview(dark).rounded_l(px(0.))))
.into_any_element(),
};
v_flex()
.id(SharedString::from(format!("scheme-{name}")))
.gap(px(8.))
.p(px(8.))
.rounded(px(18.))
.cursor_pointer()
.map(|this| if selected { this.raised(p) } else { this.hover(|style| style.bg(p.row_hover)) })
.tooltip(move |window, cx| Tooltip::new(tooltip).build(window, cx))
.on_click(move |_, window, cx| {
themes::set_mode(value, window, cx);
let _ = view.update(cx, |view, cx| view.changed(window, cx));
})
.child(
div()
.h(px(140.))
.rounded(px(14.))
.shadow(vec![if selected {
BoxShadow::new(px(0.), px(0.), p.green_deep).spread_radius(px(2.))
} else {
BoxShadow::new(px(0.), px(0.), p.rule).spread_radius(px(1.))
}])
.child(preview),
)
.child(
h_flex()
.px(px(4.))
.gap(px(8.))
.child(super::radio(selected, p))
.child(div().text_size(px(12.5)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(name)),
)
}))
}
/// A card per theme family: a swatch per variant (a sun or moon says which
/// mode it's for), the family's name, and which modes it currently owns.
fn theme_cards(light_id: &str, dark_id: &str, view: &WeakEntity<SettingsView>, cx: &App) -> Div {
let p = metal::palette(cx);
div().grid().grid_cols(3).gap(px(8.)).children(FAMILIES.iter().map(|family| {
let members: Vec<&'static Theme> = family.members().collect();
let owns_light = members.iter().any(|theme| !theme.palette.dark && theme.id == light_id);
let owns_dark = members.iter().any(|theme| theme.palette.dark && theme.id == dark_id);
let first_light = members.iter().copied().find(|theme| !theme.palette.dark);
let first_dark = members.iter().copied().find(|theme| theme.palette.dark);
let use_tooltip: SharedString = match (first_light, first_dark) {
(Some(_), Some(_)) => "Use for both light and dark".into(),
(Some(_), None) => "Use for light mode only".into(),
_ => "Use for dark mode only".into(),
};
let use_view = view.clone();
v_flex()
.min_w_0()
.rounded(px(18.))
.raised(p)
.child(
h_flex()
.h(px(84.))
.justify_center()
.gap(px(10.))
.children(members.iter().map(|theme| {
let chosen = theme.id == if theme.palette.dark { dark_id } else { light_id };
swatch(theme, chosen, view, p)
})),
)
.child(
h_flex()
.px(px(12.))
.pb(px(12.))
.pt(px(4.))
.gap(px(6.))
.child(
div()
.id(SharedString::from(format!("theme-family-{}", family.name)))
.flex_1()
.min_w_0()
.truncate()
.cursor_pointer()
.text_size(px(13.5))
.font_weight(FontWeight::MEDIUM)
.text_color(p.ink)
.hover(|style| style.text_color(p.ink2))
.tooltip(move |window, cx| Tooltip::new(use_tooltip.clone()).build(window, cx))
.on_click(move |_, window, cx| {
for theme in first_light.into_iter().chain(first_dark) {
themes::assign(theme, window, cx);
}
let _ = use_view.update(cx, |view, cx| view.changed(window, cx));
})
.child(family.name),
)
.when(owns_light, |this| this.child(metal::tag("Light", p)))
.when(owns_dark, |this| this.child(metal::tag("Dark", p))),
)
}))
}
/// One theme as a round swatch: its page and sheet, a cap and its accent.
/// Ringed when it's the theme of its mode.
fn swatch(theme: &'static Theme, chosen: bool, view: &WeakEntity<SettingsView>, p: &Palette) -> impl IntoElement {
let c = theme.palette;
let view = view.clone();
let tooltip: SharedString =
format!("Use {} for {} mode", theme.name, if c.dark { "dark" } else { "light" }).into();
div()
.id(SharedString::from(format!("theme-{}", theme.id)))
.relative()
.size(px(52.))
.flex_none()
.cursor_pointer()
.tooltip(move |window, cx| Tooltip::new(tooltip.clone()).build(window, cx))
.on_click(move |_, window, cx| {
themes::pick(theme, window, cx);
let _ = view.update(cx, |view, cx| view.changed(window, cx));
})
.child(
div()
.size_full()
.rounded_full()
.flex()
.items_center()
.justify_center()
.bg(vertical((c.s_hi, c.page)))
.shadow(c.raise_sm.clone())
.child(
div()
.size(px(22.))
.rounded_full()
.flex()
.items_center()
.justify_center()
.bg(vertical(c.cap))
.shadow(c.btn.clone())
.child(div().size(px(7.)).rounded_full().bg(c.blue_ink)),
),
)
.when(chosen, |this| {
this.child(
div().absolute().top(px(-4.)).left(px(-4.)).size(px(60.)).rounded_full().border_2().border_color(p.green_deep),
)
})
.child(
div()
.absolute()
.right(px(-2.))
.bottom(px(-2.))
.size(px(18.))
.rounded_full()
.flex()
.items_center()
.justify_center()
.bg(vertical(p.cap))
.shadow(p.raise_sm.clone())
.child(
Icon::new(if c.dark { IconName::Moon } else { IconName::Sun }).size(px(10.)).text_color(p.ink2),
),
)
}