use std::rc::Rc;
use gpui::prelude::*;
use gpui::{div, px, App, ElementId, FontWeight, Hsla, IntoElement, SharedString, Window};
use crate::devtools::Probed;
use crate::icon::{Icon, IconName};
use crate::theme::{manager, theme, ColorScheme, Size, ThemeChoice, ThemeManager};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemePickerLayout {
#[default]
List,
Grid,
}
impl ThemePickerLayout {
pub fn label(self) -> &'static str {
match self {
ThemePickerLayout::List => "list",
ThemePickerLayout::Grid => "grid",
}
}
}
type ChoiceHandler = Rc<dyn Fn(&ThemeChoice, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct ThemePicker {
layout: ThemePickerLayout,
system_option: bool,
on_change: Option<ChoiceHandler>,
}
impl Default for ThemePicker {
fn default() -> Self {
ThemePicker::new()
}
}
impl ThemePicker {
pub fn new() -> Self {
ThemePicker {
layout: ThemePickerLayout::default(),
system_option: true,
on_change: None,
}
}
pub fn layout(mut self, layout: ThemePickerLayout) -> Self {
self.layout = layout;
self
}
pub fn system_option(mut self, system_option: bool) -> Self {
self.system_option = system_option;
self
}
pub fn on_change(
mut self,
handler: impl Fn(&ThemeChoice, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
#[derive(Clone, Copy)]
struct Swatch {
body: Hsla,
surface: Hsla,
border: Hsla,
primary: Hsla,
}
impl Swatch {
fn of(theme: &crate::theme::Theme) -> Self {
Swatch {
body: theme.body().hsla(),
surface: theme.surface().hsla(),
border: theme.border().hsla(),
primary: theme.primary().hsla(),
}
}
}
struct Row {
choice: ThemeChoice,
name: SharedString,
selected: bool,
swatches: Vec<Swatch>,
}
fn swatch(colors: Swatch, width: Option<f32>, height: f32, radius: f32) -> impl IntoElement {
let mut el = div()
.flex()
.flex_col()
.justify_end()
.gap(px(3.0))
.h(px(height))
.p(px(4.0))
.rounded(px(radius))
.bg(colors.body)
.child(
div()
.h(px(4.0))
.w_full()
.rounded(px(2.0))
.bg(colors.surface),
)
.child(
div()
.h(px(4.0))
.w(px(height * 0.6))
.rounded(px(2.0))
.bg(colors.primary),
);
el = match width {
Some(width) => el.w(px(width)),
None => el.w_full(),
};
el.border_1().border_color(colors.border)
}
fn split_swatch(
colors: &[Swatch],
width: Option<f32>,
height: f32,
radius: f32,
) -> impl IntoElement {
let mut el = div().flex().gap(px(3.0)).h(px(height));
el = match width {
Some(width) => el.w(px(width)),
None => el.w_full(),
};
for half in colors {
el = el.child(div().flex_1().child(swatch(*half, None, height, radius)));
}
el
}
impl RenderOnce for ThemePicker {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = theme(cx);
let radius = t.radius(t.default_radius);
let text = t.text().hsla();
let dimmed = t.dimmed().hsla();
let accent = t.primary().hsla();
let surface = t.surface().hsla();
let surface_hover = t.surface_hover().hsla();
let border = t.border().hsla();
let selected_bg = crate::style::ColorValue::Named(t.primary_color).soft(t);
let gap = t.spacing(Size::Xs);
let mut rows: Vec<Row> = Vec::new();
if let Some(manager) = manager(cx) {
let choice = manager.selection().clone();
if self.system_option {
let light = manager
.entries()
.iter()
.find(|e| e.scheme() == ColorScheme::Light);
let dark = manager
.entries()
.iter()
.find(|e| e.scheme() == ColorScheme::Dark);
rows.push(Row {
selected: choice == ThemeChoice::System,
choice: ThemeChoice::System,
name: SharedString::new_static("System"),
swatches: [light, dark]
.into_iter()
.flatten()
.map(|e| Swatch::of(&e.theme))
.collect(),
});
}
for entry in manager.entries() {
rows.push(Row {
selected: choice == ThemeChoice::Fixed(entry.id.clone()),
choice: ThemeChoice::Fixed(entry.id.clone()),
name: entry.name.clone(),
swatches: vec![Swatch::of(&entry.theme)],
});
}
}
let grid = self.layout == ThemePickerLayout::Grid;
let mut list = div().flex().gap(px(gap));
list = if grid {
list.flex_wrap()
} else {
list.flex_col()
};
for row in rows {
let handler = self.on_change.clone();
let choice = row.choice.clone();
let id: ElementId = SharedString::from(format!("theme-picker-{choice}")).into();
let preview = if row.swatches.len() > 1 {
split_swatch(
&row.swatches,
(!grid).then_some(46.0),
if grid { 54.0 } else { 28.0 },
radius,
)
.into_any_element()
} else {
let colors = row.swatches.first().copied().unwrap_or(Swatch {
body: surface,
surface,
border,
primary: accent,
});
swatch(
colors,
(!grid).then_some(46.0),
if grid { 54.0 } else { 28.0 },
radius,
)
.into_any_element()
};
let mut item = div()
.id(id)
.cursor_pointer()
.rounded(px(radius))
.text_color(text)
.on_click(move |_, window, cx| {
ThemeManager::set_choice(cx, choice.clone());
if let Some(handler) = &handler {
handler(&choice, window, cx);
}
});
item = if grid {
item
.flex()
.flex_col()
.gap(px(6.0))
.w(px(132.0))
.p(px(8.0))
.border_1()
.border_color(if row.selected { accent } else { border })
.bg(if row.selected { selected_bg } else { surface })
.child(preview)
.child(
div()
.text_size(px(12.0))
.font_weight(if row.selected {
FontWeight::SEMIBOLD
} else {
FontWeight::NORMAL
})
.child(row.name),
)
} else {
item
.flex()
.items_center()
.gap(px(10.0))
.h(px(40.0))
.px(px(8.0))
.when(row.selected, |el| el.bg(selected_bg))
.when(!row.selected, |el| el.hover(move |s| s.bg(surface_hover)))
.child(preview)
.child(div().flex_1().text_size(px(14.0)).child(row.name))
.child(
div()
.text_color(if row.selected { accent } else { dimmed })
.when(row.selected, |el| {
el.child(Icon::new(IconName::Check).size(Size::Sm))
}),
)
};
list = list.child(item);
}
list
.probe("ThemePicker")
.attr("layout", self.layout.label())
.attr_if("system", self.system_option)
}
}