1pub use ratatui::style::{Color, Modifier, Style};
11pub use ratatui::widgets::Borders;
12
13use ratatui::widgets::{Scrollbar, ScrollbarOrientation};
14
15#[derive(Debug, Clone)]
17pub struct BaseStyle {
18 pub border: Option<BorderStyle>,
20 pub padding: Padding,
22 pub bg: Option<Color>,
24 pub fg: Option<Color>,
26}
27
28impl Default for BaseStyle {
29 fn default() -> Self {
30 Self {
31 border: Some(BorderStyle::default()),
32 padding: Padding::default(),
33 bg: None,
34 fg: Some(Color::Reset),
35 }
36 }
37}
38
39pub trait ComponentStyle {
44 fn base(&self) -> &BaseStyle;
46 fn border(&self) -> Option<&BorderStyle> {
48 self.base().border.as_ref()
49 }
50 fn padding(&self) -> &Padding {
52 &self.base().padding
53 }
54 fn bg(&self) -> Option<Color> {
56 self.base().bg
57 }
58 fn fg(&self) -> Option<Color> {
60 self.base().fg
61 }
62}
63
64#[derive(Debug, Clone, Copy, Default)]
66pub struct Padding {
67 pub top: u16,
68 pub right: u16,
69 pub bottom: u16,
70 pub left: u16,
71}
72
73impl Padding {
74 pub fn all(v: u16) -> Self {
76 Self {
77 top: v,
78 right: v,
79 bottom: v,
80 left: v,
81 }
82 }
83
84 pub fn xy(x: u16, y: u16) -> Self {
86 Self {
87 top: y,
88 right: x,
89 bottom: y,
90 left: x,
91 }
92 }
93
94 pub fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self {
96 Self {
97 top,
98 right,
99 bottom,
100 left,
101 }
102 }
103
104 pub fn horizontal(&self) -> u16 {
106 self.left + self.right
107 }
108
109 pub fn vertical(&self) -> u16 {
111 self.top + self.bottom
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct BorderStyle {
118 pub borders: Borders,
120 pub style: Style,
122 pub focused_style: Option<Style>,
124}
125
126impl Default for BorderStyle {
127 fn default() -> Self {
128 Self {
129 borders: Borders::ALL,
130 style: Style::default().fg(Color::DarkGray),
131 focused_style: Some(Style::default().fg(Color::Cyan)),
132 }
133 }
134}
135
136impl BorderStyle {
137 pub fn all() -> Self {
139 Self::default()
140 }
141
142 pub fn none() -> Self {
144 Self {
145 borders: Borders::NONE,
146 ..Default::default()
147 }
148 }
149
150 pub fn style_for_focus(&self, is_focused: bool) -> Style {
152 if is_focused {
153 self.focused_style.unwrap_or(self.style)
154 } else {
155 self.style
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct SelectionStyle {
163 pub style: Option<Style>,
165 pub marker: Option<&'static str>,
167 pub disabled: bool,
170}
171
172impl Default for SelectionStyle {
173 fn default() -> Self {
174 Self {
175 style: Some(
176 Style::default()
177 .fg(Color::Cyan)
178 .add_modifier(Modifier::BOLD),
179 ),
180 marker: Some("> "),
181 disabled: false,
182 }
183 }
184}
185
186impl SelectionStyle {
187 pub fn disabled() -> Self {
189 Self {
190 style: None,
191 marker: None,
192 disabled: true,
193 }
194 }
195
196 pub fn marker_only(marker: &'static str) -> Self {
198 Self {
199 style: None,
200 marker: Some(marker),
201 disabled: false,
202 }
203 }
204
205 pub fn style_only(style: Style) -> Self {
207 Self {
208 style: Some(style),
209 marker: None,
210 disabled: false,
211 }
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct ScrollbarStyle {
218 pub thumb: Style,
220 pub track: Style,
222 pub begin: Style,
224 pub end: Style,
226 pub thumb_symbol: Option<&'static str>,
228 pub track_symbol: Option<&'static str>,
230 pub begin_symbol: Option<&'static str>,
232 pub end_symbol: Option<&'static str>,
234}
235
236impl Default for ScrollbarStyle {
237 fn default() -> Self {
238 Self {
239 thumb: Style::default().fg(Color::Cyan),
240 track: Style::default().fg(Color::DarkGray),
241 begin: Style::default().fg(Color::DarkGray),
242 end: Style::default().fg(Color::DarkGray),
243 thumb_symbol: Some("█"),
244 track_symbol: Some("│"),
245 begin_symbol: None,
246 end_symbol: None,
247 }
248 }
249}
250
251impl ScrollbarStyle {
252 pub fn build(&self, orientation: ScrollbarOrientation) -> Scrollbar<'static> {
254 let mut scrollbar = Scrollbar::new(orientation)
255 .thumb_style(self.thumb)
256 .track_style(self.track)
257 .begin_style(self.begin)
258 .end_style(self.end)
259 .track_symbol(self.track_symbol)
260 .begin_symbol(self.begin_symbol)
261 .end_symbol(self.end_symbol);
262
263 if let Some(symbol) = self.thumb_symbol {
264 scrollbar = scrollbar.thumb_symbol(symbol);
265 }
266
267 scrollbar
268 }
269}
270
271use ratatui::text::{Line, Span};
276
277pub fn highlight_substring(
299 text: &str,
300 query: &str,
301 base_style: Style,
302 highlight_style: Style,
303) -> Line<'static> {
304 if query.is_empty() {
305 return Line::styled(text.to_string(), base_style);
306 }
307
308 if !text.is_ascii() || !query.is_ascii() {
310 return Line::styled(text.to_string(), base_style);
311 }
312
313 let text_lower = text.to_lowercase();
314 let query_lower = query.to_lowercase();
315
316 let mut spans = Vec::new();
317 let mut last_end = 0;
318
319 for (start, _) in text_lower.match_indices(&query_lower) {
320 if start > last_end {
322 spans.push(Span::styled(text[last_end..start].to_string(), base_style));
323 }
324
325 let end = start + query.len();
327 spans.push(Span::styled(text[start..end].to_string(), highlight_style));
328 last_end = end;
329 }
330
331 if last_end < text.len() {
333 spans.push(Span::styled(text[last_end..].to_string(), base_style));
334 }
335
336 if spans.is_empty() {
337 Line::styled(text.to_string(), base_style)
338 } else {
339 Line::from(spans)
340 }
341}