1use crate::{RatatuiTheme, RatatuiUiTheme};
4use clankerdiff_theme::ControlState;
5pub use clankerdiff_theme::{ButtonVariant, ModalSize, NoticeTone, SelectionState};
6use ratatui::{
7 buffer::Buffer,
8 layout::{Constraint, Layout, Rect},
9 style::Style,
10 text::{Line, Span, Text},
11 widgets::{Block, Borders, Clear, Paragraph, Widget},
12};
13
14pub(crate) const ACTION_BAR_HEIGHT: u16 = 1;
15
16#[derive(Debug, Clone, Copy)]
17pub(crate) struct FrameRegions {
18 pub body: Rect,
19 pub footer: Rect,
20}
21
22pub(crate) struct AppFrame<'a> {
23 title: &'a str,
24 borders: bool,
25 footer: bool,
26 theme: &'a RatatuiTheme,
27}
28impl<'a> AppFrame<'a> {
29 pub(crate) const fn new(title: &'a str, borders: bool, theme: &'a RatatuiTheme) -> Self {
30 Self {
31 title,
32 borders,
33 footer: true,
34 theme,
35 }
36 }
37 pub(crate) const fn footer(mut self, footer: bool) -> Self {
38 self.footer = footer;
39 self
40 }
41
42 pub(crate) fn render(self, area: Rect, buffer: &mut Buffer) -> FrameRegions {
43 buffer.set_style(
44 area,
45 Style::new().fg(self.theme.ui.text).bg(self.theme.ui.canvas),
46 );
47 let inner = if self.borders {
48 let block = Block::new()
49 .borders(Borders::ALL)
50 .title(format!(" {} ", self.title))
51 .border_style(Style::new().fg(self.theme.ui.accent));
52 let inner = block.inner(area);
53 block.render(area, buffer);
54 inner
55 } else {
56 area
57 };
58 let [body, footer] = Layout::vertical([
59 Constraint::Min(0),
60 Constraint::Length(if self.footer {
61 ACTION_BAR_HEIGHT.min(inner.height)
62 } else {
63 0
64 }),
65 ])
66 .areas(inner);
67 FrameRegions { body, footer }
68 }
69}
70
71pub(crate) struct ActionBar<'a> {
72 line: Line<'a>,
73 theme: &'a RatatuiTheme,
74}
75impl<'a> ActionBar<'a> {
76 pub(crate) const fn new(line: Line<'a>, theme: &'a RatatuiTheme) -> Self {
77 Self { line, theme }
78 }
79}
80impl Widget for ActionBar<'_> {
81 fn render(self, area: Rect, buffer: &mut Buffer) {
82 let ui = self.theme.ui.on_background(self.theme.ui.surface);
83 Paragraph::new(self.line)
84 .style(Style::new().fg(ui.text_muted).bg(ui.canvas))
85 .render(area, buffer);
86 }
87}
88
89pub struct ActionLabel<'a> {
91 key: &'a str,
92 label: &'a str,
93 variant: ButtonVariant,
94 theme: &'a RatatuiTheme,
95}
96impl<'a> ActionLabel<'a> {
97 #[must_use]
98 pub const fn new(key: &'a str, label: &'a str, theme: &'a RatatuiTheme) -> Self {
99 Self {
100 key,
101 label,
102 variant: ButtonVariant::Ghost,
103 theme,
104 }
105 }
106 #[must_use]
107 pub const fn variant(mut self, variant: ButtonVariant) -> Self {
108 self.variant = variant;
109 self
110 }
111
112 #[must_use]
113 pub fn into_span(self) -> Span<'static> {
114 Span::styled(
115 format!("[{}] {}", self.key, self.label),
116 self.theme
117 .ui
118 .control_style(self.variant, ControlState::default()),
119 )
120 }
121}
122impl Widget for ActionLabel<'_> {
123 fn render(self, area: Rect, buffer: &mut Buffer) {
124 Paragraph::new(self.into_span()).render(area, buffer);
125 }
126}
127
128pub(crate) struct Modal<'a> {
129 title: &'a str,
130 hint: Option<&'a str>,
131 size: ModalSize,
132 theme: &'a RatatuiTheme,
133}
134impl<'a> Modal<'a> {
135 pub(crate) const fn new(title: &'a str, size: ModalSize, theme: &'a RatatuiTheme) -> Self {
136 Self {
137 title,
138 hint: None,
139 size,
140 theme,
141 }
142 }
143 pub(crate) const fn hint(mut self, hint: &'a str) -> Self {
144 self.hint = Some(hint);
145 self
146 }
147 pub(crate) fn render(self, area: Rect, buffer: &mut Buffer) -> Rect {
148 let (max_width, max_height) = RatatuiUiTheme::modal_size(self.size);
149 let width = area.width.saturating_sub(4).min(max_width);
150 let height = area.height.saturating_sub(4).min(max_height);
151 let popup = Rect::new(
152 area.x + area.width.saturating_sub(width) / 2,
153 area.y + area.height.saturating_sub(height) / 2,
154 width,
155 height,
156 );
157 Clear.render(popup, buffer);
158 let block = Block::bordered()
159 .title(format!(" {} ", self.title))
160 .title_bottom(
161 self.hint
162 .map_or_else(Line::default, |hint| Line::from(format!(" {hint} "))),
163 )
164 .style(self.theme.ui.selection_style(SelectionState::None))
165 .border_style(
166 Style::new().fg(self.theme.ui.on_background(self.theme.ui.surface).border),
167 );
168 let inner = block.inner(popup);
169 block.render(popup, buffer);
170 inner
171 }
172}
173
174pub(crate) struct EmptyState<'a> {
175 text: &'a str,
176 tone: NoticeTone,
177 theme: &'a RatatuiTheme,
178}
179impl<'a> EmptyState<'a> {
180 pub(crate) const fn new(text: &'a str, tone: NoticeTone, theme: &'a RatatuiTheme) -> Self {
181 Self { text, tone, theme }
182 }
183}
184impl Widget for EmptyState<'_> {
185 fn render(self, area: Rect, buffer: &mut Buffer) {
186 Paragraph::new(self.text)
187 .style(
188 self.theme
189 .ui
190 .notice_style(self.tone)
191 .bg(self.theme.ui.canvas),
192 )
193 .render(area, buffer);
194 }
195}
196
197pub(crate) fn render_modal_text(
198 area: Rect,
199 buffer: &mut Buffer,
200 text: impl Into<Text<'static>>,
201 theme: &RatatuiTheme,
202) {
203 Paragraph::new(text)
204 .style(theme.ui.selection_style(SelectionState::None))
205 .render(area, buffer);
206}