1use std::rc::Rc;
13
14use gpui::prelude::*;
15use gpui::{div, px, App, ElementId, FontWeight, Hsla, IntoElement, SharedString, Window};
16
17use crate::devtools::Probed;
18use crate::icon::{Icon, IconName};
19use crate::theme::{manager, theme, ColorScheme, Size, ThemeChoice, ThemeManager};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum ThemePickerLayout {
24 #[default]
26 List,
27 Grid,
29}
30
31impl ThemePickerLayout {
32 pub fn label(self) -> &'static str {
33 match self {
34 ThemePickerLayout::List => "list",
35 ThemePickerLayout::Grid => "grid",
36 }
37 }
38}
39
40type ChoiceHandler = Rc<dyn Fn(&ThemeChoice, &mut Window, &mut App)>;
41
42#[derive(IntoElement)]
48pub struct ThemePicker {
49 layout: ThemePickerLayout,
50 system_option: bool,
51 on_change: Option<ChoiceHandler>,
52}
53
54impl Default for ThemePicker {
55 fn default() -> Self {
56 ThemePicker::new()
57 }
58}
59
60impl ThemePicker {
61 pub fn new() -> Self {
62 ThemePicker {
63 layout: ThemePickerLayout::default(),
64 system_option: true,
65 on_change: None,
66 }
67 }
68
69 pub fn layout(mut self, layout: ThemePickerLayout) -> Self {
70 self.layout = layout;
71 self
72 }
73
74 pub fn system_option(mut self, system_option: bool) -> Self {
77 self.system_option = system_option;
78 self
79 }
80
81 pub fn on_change(
83 mut self,
84 handler: impl Fn(&ThemeChoice, &mut Window, &mut App) + 'static,
85 ) -> Self {
86 self.on_change = Some(Rc::new(handler));
87 self
88 }
89}
90
91#[derive(Clone, Copy)]
94struct Swatch {
95 body: Hsla,
96 surface: Hsla,
97 border: Hsla,
98 primary: Hsla,
99}
100
101impl Swatch {
102 fn of(theme: &crate::theme::Theme) -> Self {
103 Swatch {
104 body: theme.body().hsla(),
105 surface: theme.surface().hsla(),
106 border: theme.border().hsla(),
107 primary: theme.primary().hsla(),
108 }
109 }
110}
111
112struct Row {
115 choice: ThemeChoice,
116 name: SharedString,
117 selected: bool,
118 swatches: Vec<Swatch>,
120}
121
122fn swatch(colors: Swatch, width: Option<f32>, height: f32, radius: f32) -> impl IntoElement {
125 let mut el = div()
126 .flex()
127 .flex_col()
128 .justify_end()
129 .gap(px(3.0))
130 .h(px(height))
131 .p(px(4.0))
132 .rounded(px(radius))
133 .bg(colors.body)
134 .child(
135 div()
136 .h(px(4.0))
137 .w_full()
138 .rounded(px(2.0))
139 .bg(colors.surface),
140 )
141 .child(
142 div()
143 .h(px(4.0))
144 .w(px(height * 0.6))
145 .rounded(px(2.0))
146 .bg(colors.primary),
147 );
148 el = match width {
149 Some(width) => el.w(px(width)),
150 None => el.w_full(),
151 };
152 el.border_1().border_color(colors.border)
153}
154
155fn split_swatch(
158 colors: &[Swatch],
159 width: Option<f32>,
160 height: f32,
161 radius: f32,
162) -> impl IntoElement {
163 let mut el = div().flex().gap(px(3.0)).h(px(height));
164 el = match width {
165 Some(width) => el.w(px(width)),
166 None => el.w_full(),
167 };
168 for half in colors {
169 el = el.child(div().flex_1().child(swatch(*half, None, height, radius)));
170 }
171 el
172}
173
174impl RenderOnce for ThemePicker {
175 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
176 let t = theme(cx);
177 let radius = t.radius(t.default_radius);
178 let text = t.text().hsla();
179 let dimmed = t.dimmed().hsla();
180 let accent = t.primary().hsla();
181 let surface = t.surface().hsla();
182 let surface_hover = t.surface_hover().hsla();
183 let border = t.border().hsla();
184 let selected_bg = crate::style::ColorValue::Named(t.primary_color).soft(t);
185 let gap = t.spacing(Size::Xs);
186
187 let mut rows: Vec<Row> = Vec::new();
188 if let Some(manager) = manager(cx) {
189 let choice = manager.selection().clone();
190 if self.system_option {
191 let light = manager
192 .entries()
193 .iter()
194 .find(|e| e.scheme() == ColorScheme::Light);
195 let dark = manager
196 .entries()
197 .iter()
198 .find(|e| e.scheme() == ColorScheme::Dark);
199 rows.push(Row {
200 selected: choice == ThemeChoice::System,
201 choice: ThemeChoice::System,
202 name: SharedString::new_static("System"),
203 swatches: [light, dark]
204 .into_iter()
205 .flatten()
206 .map(|e| Swatch::of(&e.theme))
207 .collect(),
208 });
209 }
210 for entry in manager.entries() {
211 rows.push(Row {
212 selected: choice == ThemeChoice::Fixed(entry.id.clone()),
213 choice: ThemeChoice::Fixed(entry.id.clone()),
214 name: entry.name.clone(),
215 swatches: vec![Swatch::of(&entry.theme)],
216 });
217 }
218 }
219
220 let grid = self.layout == ThemePickerLayout::Grid;
221 let mut list = div().flex().gap(px(gap));
222 list = if grid {
223 list.flex_wrap()
224 } else {
225 list.flex_col()
226 };
227
228 for row in rows {
229 let handler = self.on_change.clone();
230 let choice = row.choice.clone();
231 let id: ElementId = SharedString::from(format!("theme-picker-{choice}")).into();
232 let preview = if row.swatches.len() > 1 {
233 split_swatch(
234 &row.swatches,
235 (!grid).then_some(46.0),
236 if grid { 54.0 } else { 28.0 },
237 radius,
238 )
239 .into_any_element()
240 } else {
241 let colors = row.swatches.first().copied().unwrap_or(Swatch {
242 body: surface,
243 surface,
244 border,
245 primary: accent,
246 });
247 swatch(
248 colors,
249 (!grid).then_some(46.0),
250 if grid { 54.0 } else { 28.0 },
251 radius,
252 )
253 .into_any_element()
254 };
255
256 let mut item = div()
257 .id(id)
258 .cursor_pointer()
259 .rounded(px(radius))
260 .text_color(text)
261 .on_click(move |_, window, cx| {
262 ThemeManager::set_choice(cx, choice.clone());
263 if let Some(handler) = &handler {
264 handler(&choice, window, cx);
265 }
266 });
267
268 item = if grid {
269 item
270 .flex()
271 .flex_col()
272 .gap(px(6.0))
273 .w(px(132.0))
274 .p(px(8.0))
275 .border_1()
276 .border_color(if row.selected { accent } else { border })
277 .bg(if row.selected { selected_bg } else { surface })
278 .child(preview)
279 .child(
280 div()
281 .text_size(px(12.0))
282 .font_weight(if row.selected {
283 FontWeight::SEMIBOLD
284 } else {
285 FontWeight::NORMAL
286 })
287 .child(row.name),
288 )
289 } else {
290 item
291 .flex()
292 .items_center()
293 .gap(px(10.0))
294 .h(px(40.0))
295 .px(px(8.0))
296 .when(row.selected, |el| el.bg(selected_bg))
297 .when(!row.selected, |el| el.hover(move |s| s.bg(surface_hover)))
298 .child(preview)
299 .child(div().flex_1().text_size(px(14.0)).child(row.name))
300 .child(
301 div()
302 .text_color(if row.selected { accent } else { dimmed })
303 .when(row.selected, |el| {
304 el.child(Icon::new(IconName::Check).size(Size::Sm))
305 }),
306 )
307 };
308 list = list.child(item);
309 }
310
311 list
312 .probe("ThemePicker")
313 .attr("layout", self.layout.label())
314 .attr_if("system", self.system_option)
315 }
316}