//! 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) -> Vec { 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, |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, |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, |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, |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, 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, 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, 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), ), ) }