1use crate::event::{Key, KeyEvent};
26use crate::render::Cell;
27use crate::style::{get_theme, set_theme_by_id, theme_ids, use_theme, Color, Theme};
28use crate::widget::traits::{EventResult, Interactive, RenderContext, View, WidgetProps};
29use crate::{impl_props_builders, impl_styled_view};
30
31#[derive(Clone, Debug)]
33pub struct ThemePicker {
34 themes: Vec<String>,
36 selected_index: usize,
38 open: bool,
40 compact: bool,
42 show_preview: bool,
44 width: Option<u16>,
46 fg: Option<Color>,
48 bg: Option<Color>,
50 props: WidgetProps,
52}
53
54impl ThemePicker {
55 pub fn new() -> Self {
57 let all_themes = theme_ids();
58 let current = use_theme().get();
59 let selected_index = all_themes
60 .iter()
61 .position(|id| {
62 get_theme(id)
63 .map(|t| t.name == current.name)
64 .unwrap_or(false)
65 })
66 .unwrap_or(0);
67
68 Self {
69 themes: all_themes,
70 selected_index,
71 open: false,
72 compact: false,
73 show_preview: true,
74 width: None,
75 fg: None,
76 bg: None,
77 props: WidgetProps::new(),
78 }
79 }
80
81 pub fn themes<I, S>(mut self, theme_ids: I) -> Self
83 where
84 I: IntoIterator<Item = S>,
85 S: Into<String>,
86 {
87 self.themes = theme_ids.into_iter().map(|s| s.into()).collect();
88 self.selected_index = 0;
89 self
90 }
91
92 pub fn compact(mut self, enable: bool) -> Self {
94 self.compact = enable;
95 self
96 }
97
98 pub fn show_preview(mut self, show: bool) -> Self {
100 self.show_preview = show;
101 self
102 }
103
104 pub fn width(mut self, width: u16) -> Self {
106 self.width = Some(width);
107 self
108 }
109
110 pub fn fg(mut self, color: Color) -> Self {
112 self.fg = Some(color);
113 self
114 }
115
116 pub fn bg(mut self, color: Color) -> Self {
118 self.bg = Some(color);
119 self
120 }
121
122 pub fn toggle(&mut self) {
124 self.open = !self.open;
125 }
126
127 pub fn open(&mut self) {
129 self.open = true;
130 }
131
132 pub fn close(&mut self) {
134 self.open = false;
135 }
136
137 pub fn is_open(&self) -> bool {
139 self.open
140 }
141
142 pub fn select_prev(&mut self) {
144 if self.selected_index > 0 {
145 self.selected_index -= 1;
146 }
147 }
148
149 pub fn select_next(&mut self) {
151 if self.selected_index < self.themes.len().saturating_sub(1) {
152 self.selected_index += 1;
153 }
154 }
155
156 pub fn apply_selected(&self) {
158 if let Some(id) = self.themes.get(self.selected_index) {
159 set_theme_by_id(id);
160 }
161 }
162
163 pub fn selected_id(&self) -> Option<&str> {
165 self.themes.get(self.selected_index).map(|s| s.as_str())
166 }
167
168 pub fn selected_theme(&self) -> Option<Theme> {
170 self.selected_id().and_then(get_theme)
171 }
172
173 fn draw_swatch(&self, ctx: &mut RenderContext, x: u16, y: u16, theme: &Theme) -> u16 {
175 let swatch_colors = [
176 theme.colors.background,
177 theme.palette.primary,
178 theme.palette.success,
179 theme.palette.error,
180 ];
181
182 for (i, color) in swatch_colors.iter().enumerate() {
183 let mut cell = Cell::new(' ');
184 cell.bg = Some(*color);
185 ctx.buffer.set(x + i as u16, y, cell);
186 }
187
188 swatch_colors.len() as u16
189 }
190}
191
192impl Default for ThemePicker {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198impl View for ThemePicker {
199 fn render(&self, ctx: &mut RenderContext) {
200 let area = ctx.area;
201 if area.width < 10 || area.height < 1 {
202 return;
203 }
204
205 let current_theme = use_theme().get();
206 let width = self.width.unwrap_or(area.width.min(35));
207
208 let fg = self.fg.unwrap_or(current_theme.colors.text);
209 let bg = self.bg.unwrap_or(current_theme.colors.surface);
210
211 if self.compact {
212 self.draw_swatch(ctx, area.x, area.y, ¤t_theme);
214
215 if self.open && !self.themes.is_empty() {
216 let mut y = area.y + 1;
217 for (i, theme_id) in self.themes.iter().enumerate() {
218 if y >= area.y + area.height {
219 break;
220 }
221 if let Some(theme) = get_theme(theme_id) {
222 let selected = i == self.selected_index;
223
224 let indicator = if selected { '>' } else { ' ' };
226 let mut cell = Cell::new(indicator);
227 cell.fg = Some(theme.palette.primary);
228 ctx.buffer.set(area.x, y, cell);
229
230 self.draw_swatch(ctx, area.x + 1, y, &theme);
232 y += 1;
233 }
234 }
235 }
236 } else {
237 let mut x = area.x;
239
240 let label = "Theme: ";
242 for ch in label.chars() {
243 let mut cell = Cell::new(ch);
244 cell.fg = Some(fg);
245 cell.bg = Some(bg);
246 ctx.buffer.set(x, area.y, cell);
247 x += 1;
248 }
249
250 for ch in current_theme.name.chars() {
252 if x >= area.x + width - 6 {
253 break;
254 }
255 let mut cell = Cell::new(ch);
256 cell.fg = Some(fg);
257 cell.bg = Some(bg);
258 ctx.buffer.set(x, area.y, cell);
259 x += 1;
260 }
261
262 let mut cell = Cell::new(' ');
264 cell.bg = Some(bg);
265 ctx.buffer.set(x, area.y, cell);
266 x += 1;
267
268 x += self.draw_swatch(ctx, x, area.y, ¤t_theme);
270
271 let indicator = if self.open { " ▲" } else { " ▼" };
273 for ch in indicator.chars() {
274 let mut cell = Cell::new(ch);
275 cell.fg = Some(fg);
276 cell.bg = Some(bg);
277 ctx.buffer.set(x, area.y, cell);
278 x += 1;
279 }
280
281 while x < area.x + width {
283 let mut cell = Cell::new(' ');
284 cell.bg = Some(bg);
285 ctx.buffer.set(x, area.y, cell);
286 x += 1;
287 }
288
289 if self.open && !self.themes.is_empty() {
291 let border_color = current_theme.colors.border;
292 let mut y = area.y + 1;
293
294 if y < area.y + area.height {
296 let mut cell = Cell::new('┌');
297 cell.fg = Some(border_color);
298 ctx.buffer.set(area.x, y, cell);
299
300 for i in 1..width.saturating_sub(1) {
301 let mut cell = Cell::new('─');
302 cell.fg = Some(border_color);
303 ctx.buffer.set(area.x + i, y, cell);
304 }
305
306 let mut cell = Cell::new('┐');
307 cell.fg = Some(border_color);
308 ctx.buffer.set(area.x + width - 1, y, cell);
309 y += 1;
310 }
311
312 for (i, theme_id) in self.themes.iter().enumerate() {
314 if y >= area.y + area.height - 1 {
315 break;
316 }
317 if let Some(theme) = get_theme(theme_id) {
318 let selected = i == self.selected_index;
319 let item_bg = if selected {
320 current_theme.colors.selection
321 } else {
322 current_theme.colors.surface
323 };
324 let item_fg = if selected {
325 current_theme.colors.selection_text
326 } else {
327 current_theme.colors.text
328 };
329
330 let mut cell = Cell::new('│');
332 cell.fg = Some(border_color);
333 ctx.buffer.set(area.x, y, cell);
334
335 let mut cx = area.x + 1;
336
337 let indicator = if selected { '▶' } else { ' ' };
339 let mut cell = Cell::new(indicator);
340 cell.fg = Some(theme.palette.primary);
341 cell.bg = Some(item_bg);
342 ctx.buffer.set(cx, y, cell);
343 cx += 1;
344
345 cx += self.draw_swatch(ctx, cx, y, &theme);
347
348 let mut cell = Cell::new(' ');
350 cell.bg = Some(item_bg);
351 ctx.buffer.set(cx, y, cell);
352 cx += 1;
353
354 let max_name_len = (width as usize).saturating_sub(9);
356 for (j, ch) in theme.name.chars().enumerate() {
357 if j >= max_name_len {
358 break;
359 }
360 let mut cell = Cell::new(ch);
361 cell.fg = Some(item_fg);
362 cell.bg = Some(item_bg);
363 ctx.buffer.set(cx, y, cell);
364 cx += 1;
365 }
366
367 while cx < area.x + width - 1 {
369 let mut cell = Cell::new(' ');
370 cell.bg = Some(item_bg);
371 ctx.buffer.set(cx, y, cell);
372 cx += 1;
373 }
374
375 let mut cell = Cell::new('│');
377 cell.fg = Some(border_color);
378 ctx.buffer.set(area.x + width - 1, y, cell);
379
380 y += 1;
381 }
382 }
383
384 if y < area.y + area.height {
386 let mut cell = Cell::new('└');
387 cell.fg = Some(border_color);
388 ctx.buffer.set(area.x, y, cell);
389
390 for i in 1..width.saturating_sub(1) {
391 let mut cell = Cell::new('─');
392 cell.fg = Some(border_color);
393 ctx.buffer.set(area.x + i, y, cell);
394 }
395
396 let mut cell = Cell::new('┘');
397 cell.fg = Some(border_color);
398 ctx.buffer.set(area.x + width - 1, y, cell);
399 }
400 }
401 }
402 }
403}
404
405impl Interactive for ThemePicker {
406 fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
407 match event.key {
408 Key::Enter | Key::Char(' ') => {
409 if self.open {
410 self.apply_selected();
411 self.close();
412 } else {
413 self.open();
414 }
415 EventResult::Consumed
416 }
417 Key::Up | Key::Char('k') if self.open => {
418 self.select_prev();
419 EventResult::Consumed
420 }
421 Key::Down | Key::Char('j') if self.open => {
422 self.select_next();
423 EventResult::Consumed
424 }
425 Key::Escape if self.open => {
426 self.close();
427 EventResult::Consumed
428 }
429 Key::Tab => {
430 self.select_next();
432 if self.selected_index == 0 && !self.themes.is_empty() {
433 }
435 self.apply_selected();
436 EventResult::Consumed
437 }
438 _ => EventResult::Ignored,
439 }
440 }
441}
442
443impl_styled_view!(ThemePicker);
445impl_props_builders!(ThemePicker);
446
447pub fn theme_picker() -> ThemePicker {
449 ThemePicker::new()
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn test_theme_picker_new() {
458 let picker = ThemePicker::new();
459 assert!(!picker.themes.is_empty());
460 assert!(!picker.open);
461 }
462
463 #[test]
464 fn test_theme_picker_toggle() {
465 let mut picker = ThemePicker::new();
466 assert!(!picker.is_open());
467
468 picker.toggle();
469 assert!(picker.is_open());
470
471 picker.toggle();
472 assert!(!picker.is_open());
473 }
474
475 #[test]
476 fn test_theme_picker_selection() {
477 let mut picker = ThemePicker::new().themes(["dark", "light", "dracula"]);
478
479 assert_eq!(picker.selected_index, 0);
480
481 picker.select_next();
482 assert_eq!(picker.selected_index, 1);
483
484 picker.select_next();
485 assert_eq!(picker.selected_index, 2);
486
487 picker.select_next();
489 assert_eq!(picker.selected_index, 2);
490
491 picker.select_prev();
492 assert_eq!(picker.selected_index, 1);
493 }
494
495 #[test]
496 fn test_theme_picker_selected_id() {
497 let picker = ThemePicker::new().themes(["dracula", "nord"]);
498
499 assert_eq!(picker.selected_id(), Some("dracula"));
500 }
501
502 #[test]
503 fn test_theme_picker_compact() {
504 let picker = ThemePicker::new().compact(true);
505 assert!(picker.compact);
506 }
507
508 #[test]
509 fn test_theme_picker_custom_themes() {
510 let picker = ThemePicker::new().themes(["dark", "nord"]);
511
512 assert_eq!(picker.themes.len(), 2);
513 assert_eq!(picker.themes[0], "dark");
514 assert_eq!(picker.themes[1], "nord");
515 }
516
517 #[test]
518 fn test_theme_picker_width() {
519 let picker = ThemePicker::new().width(50);
520 assert_eq!(picker.width, Some(50));
521 }
522
523 #[test]
524 fn test_theme_picker_handle_key_open() {
525 let mut picker = ThemePicker::new();
526
527 let event = KeyEvent::new(Key::Enter);
528 let result = picker.handle_key(&event);
529
530 assert_eq!(result, EventResult::Consumed);
531 assert!(picker.is_open());
532 }
533
534 #[test]
535 fn test_theme_picker_handle_key_close() {
536 let mut picker = ThemePicker::new();
537 picker.open();
538
539 let event = KeyEvent::new(Key::Escape);
540 let result = picker.handle_key(&event);
541
542 assert_eq!(result, EventResult::Consumed);
543 assert!(!picker.is_open());
544 }
545
546 #[test]
547 fn test_theme_picker_handle_key_navigate() {
548 let mut picker = ThemePicker::new().themes(["dark", "light", "dracula"]);
549 picker.open();
550
551 let event = KeyEvent::new(Key::Down);
553 picker.handle_key(&event);
554 assert_eq!(picker.selected_index, 1);
555
556 let event = KeyEvent::new(Key::Up);
558 picker.handle_key(&event);
559 assert_eq!(picker.selected_index, 0);
560
561 let event = KeyEvent::new(Key::Char('j'));
563 picker.handle_key(&event);
564 assert_eq!(picker.selected_index, 1);
565
566 let event = KeyEvent::new(Key::Char('k'));
568 picker.handle_key(&event);
569 assert_eq!(picker.selected_index, 0);
570 }
571}