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 Paragraph::new(self.line)
83 .style(
84 Style::new()
85 .fg(self.theme.ui.text_muted)
86 .bg(self.theme.ui.surface),
87 )
88 .render(area, buffer);
89 }
90}
91
92pub struct ActionLabel<'a> {
94 key: &'a str,
95 label: &'a str,
96 variant: ButtonVariant,
97 theme: &'a RatatuiTheme,
98}
99impl<'a> ActionLabel<'a> {
100 #[must_use]
101 pub const fn new(key: &'a str, label: &'a str, theme: &'a RatatuiTheme) -> Self {
102 Self {
103 key,
104 label,
105 variant: ButtonVariant::Ghost,
106 theme,
107 }
108 }
109 #[must_use]
110 pub const fn variant(mut self, variant: ButtonVariant) -> Self {
111 self.variant = variant;
112 self
113 }
114
115 #[must_use]
116 pub fn into_span(self) -> Span<'static> {
117 Span::styled(
118 format!("[{}] {}", self.key, self.label),
119 self.theme
120 .ui
121 .control_style(self.variant, ControlState::default()),
122 )
123 }
124}
125impl Widget for ActionLabel<'_> {
126 fn render(self, area: Rect, buffer: &mut Buffer) {
127 Paragraph::new(self.into_span()).render(area, buffer);
128 }
129}
130
131pub(crate) struct Modal<'a> {
132 title: &'a str,
133 hint: Option<&'a str>,
134 size: ModalSize,
135 theme: &'a RatatuiTheme,
136}
137impl<'a> Modal<'a> {
138 pub(crate) const fn new(title: &'a str, size: ModalSize, theme: &'a RatatuiTheme) -> Self {
139 Self {
140 title,
141 hint: None,
142 size,
143 theme,
144 }
145 }
146 pub(crate) const fn hint(mut self, hint: &'a str) -> Self {
147 self.hint = Some(hint);
148 self
149 }
150 pub(crate) fn render(self, area: Rect, buffer: &mut Buffer) -> Rect {
151 let (max_width, max_height) = RatatuiUiTheme::modal_size(self.size);
152 let width = area.width.saturating_sub(4).min(max_width);
153 let height = area.height.saturating_sub(4).min(max_height);
154 let popup = Rect::new(
155 area.x + area.width.saturating_sub(width) / 2,
156 area.y + area.height.saturating_sub(height) / 2,
157 width,
158 height,
159 );
160 Clear.render(popup, buffer);
161 let block = Block::bordered()
162 .title(format!(" {} ", self.title))
163 .title_bottom(
164 self.hint
165 .map_or_else(Line::default, |hint| Line::from(format!(" {hint} "))),
166 )
167 .style(
168 Style::new()
169 .fg(self.theme.ui.text)
170 .bg(self.theme.ui.surface),
171 )
172 .border_style(Style::new().fg(self.theme.ui.border));
173 let inner = block.inner(popup);
174 block.render(popup, buffer);
175 inner
176 }
177}
178
179pub(crate) struct EmptyState<'a> {
180 text: &'a str,
181 tone: NoticeTone,
182 theme: &'a RatatuiTheme,
183}
184impl<'a> EmptyState<'a> {
185 pub(crate) const fn new(text: &'a str, tone: NoticeTone, theme: &'a RatatuiTheme) -> Self {
186 Self { text, tone, theme }
187 }
188}
189impl Widget for EmptyState<'_> {
190 fn render(self, area: Rect, buffer: &mut Buffer) {
191 Paragraph::new(self.text)
192 .style(
193 self.theme
194 .ui
195 .notice_style(self.tone)
196 .bg(self.theme.ui.canvas),
197 )
198 .render(area, buffer);
199 }
200}
201
202pub(crate) fn render_modal_text(
203 area: Rect,
204 buffer: &mut Buffer,
205 text: impl Into<Text<'static>>,
206 theme: &RatatuiTheme,
207) {
208 Paragraph::new(text)
209 .style(Style::new().fg(theme.ui.text).bg(theme.ui.surface))
210 .render(area, buffer);
211}