1use crate::{
2 app::{
3 state::{Focus, KeyBindingEnum},
4 App,
5 },
6 constants::LIST_SELECTED_SYMBOL,
7 ui::{
8 rendering::{
9 common::{render_blank_styled_canvas, render_close_button},
10 popup::EditThemeStyle,
11 utils::{
12 centered_rect_with_percentage, check_if_active_and_get_style,
13 check_if_mouse_is_in_area, get_mouse_focusable_field_style,
14 },
15 },
16 theme::{Theme, ThemeEnum},
17 Renderable, TextColorOptions, TextModifierOptions,
18 },
19 util::parse_hex_to_rgb,
20};
21use log::debug;
22use ratatui::{
23 layout::{Alignment, Constraint, Direction, Layout},
24 style::{Color, Style},
25 text::{Line, Span},
26 widgets::{Block, BorderType, Borders, List, ListItem, Paragraph},
27 Frame,
28};
29use strum::IntoEnumIterator;
30
31impl Renderable for EditThemeStyle {
32 fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
33 let popup_area = centered_rect_with_percentage(90, 80, rect.area());
34 let main_chunks = Layout::default()
35 .direction(Direction::Vertical)
36 .constraints(
37 [
38 Constraint::Fill(1),
39 Constraint::Length(4),
40 Constraint::Length(3),
41 ]
42 .as_ref(),
43 )
44 .margin(1)
45 .split(popup_area);
46 let chunks = Layout::default()
47 .direction(Direction::Horizontal)
48 .constraints(
49 [
50 Constraint::Fill(1),
51 Constraint::Fill(1),
52 Constraint::Fill(1),
53 ]
54 .as_ref(),
55 )
56 .split(main_chunks[0]);
57
58 fn set_foreground_color(app: &App, style: &mut Style) {
59 if let Some(fg_selected) = app.state.app_list_states.edit_specific_style[0].selected() {
60 if let Some(fg_color) = TextColorOptions::iter().nth(fg_selected).map(Color::from) {
61 if let Color::Rgb(_, _, _) = fg_color {
62 let user_input = app
63 .state
64 .text_buffers
65 .theme_editor_fg_hex
66 .get_joined_lines();
67 let parsed_hex = parse_hex_to_rgb(&user_input);
68 if let Some((r, g, b)) = parsed_hex {
69 style.fg = Some(Color::Rgb(r, g, b));
70 return;
71 }
72 }
73 style.fg = Some(fg_color);
74 }
75 }
76 }
77
78 fn set_background_color(app: &mut App, style: &mut Style) {
79 if let Some(bg_selected) = app.state.app_list_states.edit_specific_style[1].selected() {
80 if let Some(bg_color) = TextColorOptions::iter().nth(bg_selected).map(Color::from) {
81 if let Color::Rgb(_, _, _) = bg_color {
82 let user_input = app
83 .state
84 .text_buffers
85 .theme_editor_bg_hex
86 .get_joined_lines();
87 let parsed_hex = parse_hex_to_rgb(&user_input);
88 if let Some((r, g, b)) = parsed_hex {
89 style.bg = Some(Color::Rgb(r, g, b));
90 return;
91 }
92 }
93 style.bg = Some(bg_color);
94 }
95 }
96 }
97
98 fn set_text_modifier(app: &mut App, style: &mut Style) {
99 if let Some(modifier) = app.state.app_list_states.edit_specific_style[2].selected() {
100 if let Some(modifier) = TextModifierOptions::iter()
101 .nth(modifier)
102 .map(ratatui::style::Modifier::from)
103 {
104 Theme::add_modifier_to_style(style, modifier);
105 }
106 }
107 }
108
109 fn create_list_item_from_color<'a>(
110 color: TextColorOptions,
111 style: Style,
112 app: &mut App,
113 is_active: bool,
114 ) -> ListItem<'a> {
115 let text_style = check_if_active_and_get_style(
116 is_active,
117 app.current_theme.inactive_text_style,
118 style,
119 );
120 let general_style = check_if_active_and_get_style(
121 is_active,
122 app.current_theme.inactive_text_style,
123 app.current_theme.general_style,
124 );
125 ListItem::new(vec![Line::from(vec![
126 Span::styled("Sample Text", text_style),
127 Span::styled(format!(" - {}", color), general_style),
128 ])])
129 }
130
131 fn handle_custom_hex_input<'a>(
132 hex_value: String,
133 mut style: Style,
134 app: &mut App,
135 is_active: bool,
136 ) -> Option<ListItem<'a>> {
137 if let Some((red_channel, green_channel, blue_channel)) = parse_hex_to_rgb(&hex_value) {
138 let color = TextColorOptions::HEX(red_channel, green_channel, blue_channel);
139 style.fg = Some(Color::from(color));
140 Some(create_list_item_from_color(color, style, app, is_active))
141 } else {
142 None
143 }
144 }
145
146 let general_style = check_if_active_and_get_style(
147 is_active,
148 app.current_theme.inactive_text_style,
149 app.current_theme.general_style,
150 );
151 let help_key_style = check_if_active_and_get_style(
152 is_active,
153 app.current_theme.inactive_text_style,
154 app.current_theme.help_key_style,
155 );
156 let help_text_style = check_if_active_and_get_style(
157 is_active,
158 app.current_theme.inactive_text_style,
159 app.current_theme.help_text_style,
160 );
161 let fg_list_border_style = if !is_active {
164 app.current_theme.inactive_text_style
165 } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[0]) {
166 if app.state.app_list_states.edit_specific_style[0]
167 .selected()
168 .is_none()
169 {
170 app.state.app_list_states.edit_specific_style[0].select(Some(0));
171 }
172 app.state.mouse_focus = Some(Focus::StyleEditorFG);
173 app.state.set_focus(Focus::StyleEditorFG);
174 app.current_theme.mouse_focus_style
175 } else if app.state.focus == Focus::StyleEditorFG {
176 app.current_theme.keyboard_focus_style
177 } else {
178 app.current_theme.general_style
179 };
180 let bg_list_border_style = if !is_active {
182 app.current_theme.inactive_text_style
183 } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[1]) {
184 if app.state.app_list_states.edit_specific_style[1]
185 .selected()
186 .is_none()
187 {
188 app.state.app_list_states.edit_specific_style[1].select(Some(0));
189 }
190 app.state.mouse_focus = Some(Focus::StyleEditorBG);
191 app.state.set_focus(Focus::StyleEditorBG);
192 app.current_theme.mouse_focus_style
193 } else if app.state.focus == Focus::StyleEditorBG {
194 app.current_theme.keyboard_focus_style
195 } else {
196 app.current_theme.general_style
197 };
198 let modifiers_list_border_style = if !is_active {
200 app.current_theme.inactive_text_style
201 } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[2]) {
202 if app.state.app_list_states.edit_specific_style[2]
203 .selected()
204 .is_none()
205 {
206 app.state.app_list_states.edit_specific_style[2].select(Some(0));
207 }
208 app.state.mouse_focus = Some(Focus::StyleEditorModifier);
209 app.state.set_focus(Focus::StyleEditorModifier);
210 app.current_theme.mouse_focus_style
211 } else if app.state.focus == Focus::StyleEditorModifier {
212 app.current_theme.keyboard_focus_style
213 } else {
214 app.current_theme.general_style
215 };
216 let submit_button_style = get_mouse_focusable_field_style(
217 app,
218 Focus::SubmitButton,
219 &main_chunks[1],
220 is_active,
221 false,
222 );
223 let fg_list_items: Vec<ListItem> = TextColorOptions::iter()
224 .map(|color| {
225 let mut fg_style = Style::default();
226 let current_user_input = app
227 .state
228 .text_buffers
229 .theme_editor_fg_hex
230 .get_joined_lines();
231
232 set_background_color(app, &mut fg_style);
233 set_text_modifier(app, &mut fg_style);
234
235 if let TextColorOptions::HEX(_, _, _) = color {
236 if current_user_input.is_empty() {
237 fg_style.fg = Some(Color::Rgb(0, 0, 0));
238 return create_list_item_from_color(
239 TextColorOptions::HEX(0, 0, 0),
240 fg_style,
241 app,
242 is_active,
243 );
244 } else if let Some(list_item) =
245 handle_custom_hex_input(current_user_input, fg_style, app, is_active)
246 {
247 return list_item;
248 }
249 }
250 fg_style.fg = Some(Color::from(color));
251 create_list_item_from_color(color, fg_style, app, is_active)
252 })
253 .collect();
254
255 let bg_list_items: Vec<ListItem> = TextColorOptions::iter()
256 .map(|color| {
257 let mut bg_style = Style::default();
258 let current_user_input = app
259 .state
260 .text_buffers
261 .theme_editor_bg_hex
262 .get_joined_lines();
263
264 set_foreground_color(app, &mut bg_style);
265 set_text_modifier(app, &mut bg_style);
266
267 if let TextColorOptions::HEX(_, _, _) = color {
268 if current_user_input.is_empty() {
269 bg_style.bg = Some(Color::Rgb(0, 0, 0));
270 return create_list_item_from_color(
271 TextColorOptions::HEX(0, 0, 0),
272 bg_style,
273 app,
274 is_active,
275 );
276 } else if let Some(list_item) =
277 handle_custom_hex_input(current_user_input, bg_style, app, is_active)
278 {
279 return list_item;
280 }
281 }
282 bg_style.bg = Some(Color::from(color));
283 create_list_item_from_color(color, bg_style, app, is_active)
284 })
285 .collect();
286
287 let modifier_list_items: Vec<ListItem> = TextModifierOptions::iter()
288 .map(|modifier| {
289 let mut modifier_style = general_style;
290 if is_active {
291 set_foreground_color(app, &mut modifier_style);
292 set_background_color(app, &mut modifier_style);
293 }
294
295 Theme::add_modifier_to_style(
296 &mut modifier_style,
297 ratatui::style::Modifier::from(modifier.clone()),
298 );
299 ListItem::new(vec![Line::from(vec![
300 Span::styled("Sample Text", modifier_style),
301 Span::styled(format!(" - {}", modifier), general_style),
302 ])])
303 })
304 .collect();
305
306 let fg_list = if is_active {
307 List::new(fg_list_items)
308 .block(
309 Block::default()
310 .borders(Borders::ALL)
311 .border_type(BorderType::Rounded)
312 .title("Foreground")
313 .border_style(fg_list_border_style),
314 )
315 .highlight_symbol(LIST_SELECTED_SYMBOL)
316 } else {
317 List::new(fg_list_items).block(
318 Block::default()
319 .borders(Borders::ALL)
320 .border_type(BorderType::Rounded)
321 .title("Foreground")
322 .border_style(fg_list_border_style),
323 )
324 };
325
326 let bg_list = if is_active {
327 List::new(bg_list_items)
328 .block(
329 Block::default()
330 .borders(Borders::ALL)
331 .border_type(BorderType::Rounded)
332 .title("Background")
333 .border_style(bg_list_border_style),
334 )
335 .highlight_symbol(LIST_SELECTED_SYMBOL)
336 } else {
337 List::new(bg_list_items).block(
338 Block::default()
339 .borders(Borders::ALL)
340 .border_type(BorderType::Rounded)
341 .title("Background")
342 .border_style(bg_list_border_style),
343 )
344 };
345
346 let modifier_list = if is_active {
347 List::new(modifier_list_items)
348 .block(
349 Block::default()
350 .borders(Borders::ALL)
351 .border_type(BorderType::Rounded)
352 .title("Modifiers")
353 .border_style(modifiers_list_border_style),
354 )
355 .highlight_symbol(LIST_SELECTED_SYMBOL)
356 } else {
357 List::new(modifier_list_items).block(
358 Block::default()
359 .borders(Borders::ALL)
360 .border_type(BorderType::Rounded)
361 .title("Modifiers")
362 .border_style(modifiers_list_border_style),
363 )
364 };
365
366 let theme_style_being_edited =
367 if let Some(index) = app.state.app_table_states.theme_editor.selected() {
368 if let Some(theme_enum) = ThemeEnum::iter().nth(index) {
369 theme_enum.to_string()
370 } else {
371 debug!("Index is out of bounds for theme_style_being_edited");
372 "None".to_string()
373 }
374 } else {
375 "None".to_string()
376 };
377 let border_block = Block::default()
378 .title(format!("Editing Style: {}", theme_style_being_edited))
379 .borders(Borders::ALL)
380 .border_type(BorderType::Rounded)
381 .border_style(general_style);
382
383 let submit_button = Paragraph::new("Confirm Changes")
384 .style(submit_button_style)
385 .block(
386 Block::default()
387 .borders(Borders::ALL)
388 .border_type(BorderType::Rounded)
389 .border_style(submit_button_style),
390 )
391 .alignment(Alignment::Center);
392
393 let up_key = app
394 .get_first_keybinding(KeyBindingEnum::Up)
395 .unwrap_or("".to_string());
396 let down_key = app
397 .get_first_keybinding(KeyBindingEnum::Down)
398 .unwrap_or("".to_string());
399 let next_focus_key = app
400 .get_first_keybinding(KeyBindingEnum::NextFocus)
401 .unwrap_or("".to_string());
402 let prv_focus_key = app
403 .get_first_keybinding(KeyBindingEnum::PrvFocus)
404 .unwrap_or("".to_string());
405 let accept_key = app
406 .get_first_keybinding(KeyBindingEnum::Accept)
407 .unwrap_or("".to_string());
408
409 let help_spans = vec![
410 Span::styled("Use ", help_text_style),
411 Span::styled(up_key, help_key_style),
412 Span::styled(" and ", help_text_style),
413 Span::styled(down_key, help_key_style),
414 Span::styled(" or scroll with the mouse", help_text_style),
415 Span::styled(" to select a Color/Modifier, Press ", help_text_style),
416 Span::styled(accept_key, help_key_style),
417 Span::styled(" or ", help_text_style),
418 Span::styled("<Mouse Left Click>", help_key_style),
419 Span::styled(" to edit (for custom RBG), Press ", help_text_style),
420 Span::styled(next_focus_key, help_key_style),
421 Span::styled(" or ", help_text_style),
422 Span::styled(prv_focus_key, help_key_style),
423 Span::styled(" to change focus.", help_text_style),
424 ];
425
426 let help_text = Paragraph::new(Line::from(help_spans))
427 .block(
428 Block::default()
429 .borders(Borders::ALL)
430 .border_type(BorderType::Rounded)
431 .border_style(help_text_style),
432 )
433 .alignment(Alignment::Center)
434 .wrap(ratatui::widgets::Wrap { trim: true });
435
436 render_blank_styled_canvas(rect, &app.current_theme, popup_area, is_active);
437 rect.render_stateful_widget(
438 fg_list,
439 chunks[0],
440 &mut app.state.app_list_states.edit_specific_style[0],
441 );
442 rect.render_stateful_widget(
443 bg_list,
444 chunks[1],
445 &mut app.state.app_list_states.edit_specific_style[1],
446 );
447 rect.render_stateful_widget(
448 modifier_list,
449 chunks[2],
450 &mut app.state.app_list_states.edit_specific_style[2],
451 );
452 rect.render_widget(help_text, main_chunks[1]);
453 rect.render_widget(submit_button, main_chunks[2]);
454 rect.render_widget(border_block, popup_area);
455 if app.config.enable_mouse_support {
456 render_close_button(rect, app, is_active)
457 }
458 }
459}