Skip to main content

basalt_tui/
theme_selector_modal.rs

1use std::marker::PhantomData;
2
3use ratatui::{
4    buffer::Buffer,
5    layout::{Constraint, Flex, Layout, Rect},
6    widgets::{BorderType, Clear, StatefulWidget, Widget},
7};
8
9use crate::{
10    app::Message as AppMessage,
11    config::Theme,
12    theme_selector::{ThemeSelector, ThemeSelectorState},
13};
14
15#[derive(Clone, Debug, PartialEq)]
16pub enum Message {
17    Toggle,
18    Up,
19    Down,
20    Select,
21    Close,
22}
23
24/// Drives the theme picker. Navigating previews the highlighted theme live;
25/// `Select` keeps it, while `Close` (and toggling shut) reverts to whatever was
26/// active when the picker opened, mirroring Helix's `:theme`.
27pub fn update<'a>(
28    message: &Message,
29    state: &mut ThemeSelectorModalState,
30    current: Theme,
31) -> Option<AppMessage<'a>> {
32    match message {
33        Message::Toggle if !state.visible => {
34            state.open(current);
35            state.preview()
36        }
37        Message::Toggle | Message::Close => {
38            state.hide();
39            Some(AppMessage::PreviewTheme(state.original))
40        }
41        Message::Up => {
42            state.theme_selector_state.previous();
43            state.preview()
44        }
45        Message::Down => {
46            state.theme_selector_state.next();
47            state.preview()
48        }
49        Message::Select => {
50            let name = state
51                .theme_selector_state
52                .selected_name()
53                .map(str::to_string);
54            state.hide();
55            name.map(AppMessage::SaveTheme)
56        }
57    }
58}
59
60#[derive(Debug, Default, Clone, PartialEq)]
61pub struct ThemeSelectorModalState {
62    pub theme_selector_state: ThemeSelectorState,
63    pub visible: bool,
64    original: Theme,
65}
66
67impl ThemeSelectorModalState {
68    pub fn new(items: Vec<(String, Theme)>) -> Self {
69        Self {
70            theme_selector_state: ThemeSelectorState::new(items),
71            visible: false,
72            original: Theme::default(),
73        }
74    }
75
76    fn open(&mut self, current: Theme) {
77        self.original = current;
78        self.theme_selector_state.select_theme(&current);
79        self.visible = true;
80    }
81
82    fn hide(&mut self) {
83        self.visible = false;
84    }
85
86    fn preview<'a>(&self) -> Option<AppMessage<'a>> {
87        self.theme_selector_state
88            .selected_theme()
89            .map(AppMessage::PreviewTheme)
90    }
91}
92
93pub struct ThemeSelectorModal<'a> {
94    _lifetime: PhantomData<&'a ()>,
95    pub border_type: BorderType,
96    pub theme: Theme,
97}
98
99impl<'a> ThemeSelectorModal<'a> {
100    pub fn new(border_type: BorderType, theme: Theme) -> Self {
101        Self {
102            _lifetime: PhantomData,
103            border_type,
104            theme,
105        }
106    }
107
108    fn modal_area(&self, area: Rect) -> Rect {
109        let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
110        let horizontal = Layout::horizontal([Constraint::Length(40)]).flex(Flex::Center);
111        let [area] = vertical.areas(area);
112        let [area] = horizontal.areas(area);
113        area
114    }
115}
116
117impl<'a> StatefulWidget for ThemeSelectorModal<'a> {
118    type State = ThemeSelectorModalState;
119
120    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
121        let area = self.modal_area(area);
122        Widget::render(Clear, area, buf);
123        ThemeSelector::new(self.border_type, self.theme).render(
124            area,
125            buf,
126            &mut state.theme_selector_state,
127        );
128    }
129}