1use crate::_private::NonExhaustive;
7use crate::util::revert_style;
8use rat_event::util::MouseFlags;
9use rat_event::{HandleEvent, Outcome, ct_event};
10use rat_focus::{Focus, FocusFlag, HasFocus};
11use rat_reloc::{RelocatableState, relocate_area};
12use ratatui::buffer::Buffer;
13use ratatui::layout::{Alignment, Rect};
14use ratatui::style::{Style, Stylize};
15use ratatui::text::{Line, Span};
16use ratatui::widgets::{StatefulWidget, Widget};
17use std::borrow::Cow;
18use std::ops::Range;
19use unicode_segmentation::UnicodeSegmentation;
20
21#[derive(Debug, Clone)]
27pub struct Caption<'a> {
28 text: Cow<'a, str>,
30 highlight: Option<Range<usize>>,
32 navchar: Option<char>,
34 hotkey_text: Cow<'a, str>,
36 hotkey_align: HotkeyAlignment,
38 hotkey_policy: HotkeyPolicy,
40 hotkey: Option<crossterm::event::KeyEvent>,
42 spacing: u16,
44 align: Alignment,
46
47 linked: Option<FocusFlag>,
49
50 style: Style,
51 hover_style: Option<Style>,
52 highlight_style: Option<Style>,
53 hotkey_style: Option<Style>,
54 focus_style: Option<Style>,
55}
56
57#[derive(Debug)]
58pub struct CaptionState {
59 pub area: Rect,
62 pub navchar: Option<char>,
65 pub hotkey: Option<crossterm::event::KeyEvent>,
68
69 pub linked: FocusFlag,
71
72 pub mouse: MouseFlags,
75
76 pub non_exhaustive: NonExhaustive,
77}
78
79#[derive(Debug, Clone)]
81pub struct CaptionStyle {
82 pub style: Style,
84 pub hover: Option<Style>,
86 pub highlight: Option<Style>,
88 pub hotkey: Option<Style>,
90 pub focus: Option<Style>,
92
93 pub align: Option<Alignment>,
95 pub hotkey_align: Option<HotkeyAlignment>,
97 pub hotkey_policy: Option<HotkeyPolicy>,
99 pub spacing: Option<u16>,
101
102 pub non_exhaustive: NonExhaustive,
103}
104
105#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
107pub enum HotkeyPolicy {
108 #[default]
110 Always,
111 OnHover,
113 WhenFocused,
115}
116
117#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
119pub enum HotkeyAlignment {
120 #[default]
122 LabelHotkey,
123 HotkeyLabel,
125}
126
127impl<'a> Default for Caption<'a> {
128 fn default() -> Self {
129 Self {
130 text: Default::default(),
131 highlight: Default::default(),
132 navchar: Default::default(),
133 hotkey_text: Default::default(),
134 hotkey_align: Default::default(),
135 hotkey_policy: Default::default(),
136 hotkey: Default::default(),
137 spacing: 1,
138 align: Default::default(),
139 linked: Default::default(),
140 style: Default::default(),
141 hover_style: Default::default(),
142 highlight_style: Default::default(),
143 hotkey_style: Default::default(),
144 focus_style: Default::default(),
145 }
146 }
147}
148
149impl<'a> Caption<'a> {
150 pub fn new() -> Self {
152 Default::default()
153 }
154
155 pub fn parse(txt: &'a str) -> Self {
162 let mut zelf = Caption::default();
163
164 let mut idx_underscore = None;
165 let mut idx_navchar_start = None;
166 let mut idx_navchar_end = None;
167 let mut idx_pipe = None;
168
169 let cit = txt.char_indices();
170 for (idx, c) in cit {
171 if idx_underscore.is_none() && c == '_' {
172 idx_underscore = Some(idx);
173 } else if idx_underscore.is_some() && idx_navchar_start.is_none() {
174 idx_navchar_start = Some(idx);
175 } else if idx_navchar_start.is_some() && idx_navchar_end.is_none() {
176 idx_navchar_end = Some(idx);
177 }
178 if c == '|' {
179 idx_pipe = Some(idx);
180 }
181 }
182 if idx_navchar_start.is_some() && idx_navchar_end.is_none() {
183 idx_navchar_end = Some(txt.len());
184 }
185
186 if let Some(pipe) = idx_pipe {
187 if let Some(navchar_end) = idx_navchar_end {
188 if navchar_end > pipe {
189 idx_pipe = None;
190 }
191 }
192 }
193
194 let (text, hotkey_text) = if let Some(idx_pipe) = idx_pipe {
195 (&txt[..idx_pipe], &txt[idx_pipe + 1..])
196 } else {
197 (txt, "")
198 };
199
200 if let Some(idx_navchar_start) = idx_navchar_start {
201 if let Some(idx_navchar_end) = idx_navchar_end {
202 zelf.text = Cow::Borrowed(text);
203 zelf.highlight = Some(idx_navchar_start..idx_navchar_end);
204 zelf.navchar = Some(
205 text[idx_navchar_start..idx_navchar_end]
206 .chars()
207 .next()
208 .expect("char")
209 .to_ascii_lowercase(),
210 );
211 zelf.hotkey_text = Cow::Borrowed(hotkey_text);
212 } else {
213 unreachable!();
214 }
215 } else {
216 zelf.text = Cow::Borrowed(text);
217 zelf.highlight = None;
218 zelf.navchar = None;
219 zelf.hotkey_text = Cow::Borrowed(hotkey_text);
220 }
221
222 zelf
223 }
224
225 pub fn text(mut self, txt: &'a str) -> Self {
231 self.text = Cow::Borrowed(txt);
232 self
233 }
234
235 pub fn spacing(mut self, spacing: u16) -> Self {
237 self.spacing = spacing;
238 self
239 }
240
241 pub fn hotkey(mut self, hotkey: crossterm::event::KeyEvent) -> Self {
243 self.hotkey = Some(hotkey);
244 self
245 }
246
247 pub fn hotkey_text(mut self, hotkey: &'a str) -> Self {
249 self.hotkey_text = Cow::Borrowed(hotkey);
250 self
251 }
252
253 pub fn hotkey_align(mut self, align: HotkeyAlignment) -> Self {
255 self.hotkey_align = align;
256 self
257 }
258
259 pub fn hotkey_policy(mut self, policy: HotkeyPolicy) -> Self {
261 self.hotkey_policy = policy;
262 self
263 }
264
265 pub fn link(mut self, widget: &impl HasFocus) -> Self {
267 self.linked = Some(widget.focus());
268 self
269 }
270
271 pub fn highlight(mut self, bytes: Range<usize>) -> Self {
273 self.highlight = Some(bytes);
274 self
275 }
276
277 pub fn navchar(mut self, navchar: char) -> Self {
279 self.navchar = Some(navchar);
280 self
281 }
282
283 pub fn align(mut self, align: Alignment) -> Self {
285 self.align = align;
286 self
287 }
288
289 pub fn styles(mut self, styles: CaptionStyle) -> Self {
291 self.style = styles.style;
292 if styles.hover.is_some() {
293 self.hover_style = styles.hover;
294 }
295 if styles.highlight.is_some() {
296 self.highlight_style = styles.highlight;
297 }
298 if styles.hotkey.is_some() {
299 self.hotkey_style = styles.hotkey;
300 }
301 if styles.focus.is_some() {
302 self.focus_style = styles.focus;
303 }
304 if let Some(spacing) = styles.spacing {
305 self.spacing = spacing;
306 }
307 if let Some(align) = styles.hotkey_align {
308 self.hotkey_align = align;
309 }
310 if let Some(align) = styles.align {
311 self.align = align;
312 }
313 if let Some(hotkey_policy) = styles.hotkey_policy {
314 self.hotkey_policy = hotkey_policy;
315 }
316 self
317 }
318
319 #[inline]
321 pub fn style(mut self, style: Style) -> Self {
322 self.style = style;
323 self
324 }
325
326 #[inline]
328 pub fn hover_style(mut self, style: Style) -> Self {
329 self.hover_style = Some(style);
330 self
331 }
332
333 #[inline]
335 pub fn hover_opt(mut self, style: Option<Style>) -> Self {
336 self.hover_style = style;
337 self
338 }
339
340 #[inline]
342 pub fn highlight_style(mut self, style: Style) -> Self {
343 self.highlight_style = Some(style);
344 self
345 }
346
347 #[inline]
349 pub fn highlight_style_opt(mut self, style: Option<Style>) -> Self {
350 self.highlight_style = style;
351 self
352 }
353
354 #[inline]
356 pub fn hotkey_style(mut self, style: Style) -> Self {
357 self.hotkey_style = Some(style);
358 self
359 }
360
361 #[inline]
363 pub fn hotkey_style_opt(mut self, style: Option<Style>) -> Self {
364 self.hotkey_style = style;
365 self
366 }
367
368 #[inline]
370 pub fn focus_style(mut self, style: Style) -> Self {
371 self.focus_style = Some(style);
372 self
373 }
374
375 #[inline]
377 pub fn focus_style_opt(mut self, style: Option<Style>) -> Self {
378 self.focus_style = style;
379 self
380 }
381
382 pub fn text_width(&self) -> u16 {
384 self.text.graphemes(true).count() as u16
385 }
386
387 pub fn hotkey_width(&self) -> u16 {
389 self.hotkey_text.graphemes(true).count() as u16
390 }
391
392 pub fn width(&self) -> u16 {
394 self.text_width() + self.hotkey_width()
395 }
396
397 pub fn height(&self) -> u16 {
399 1
400 }
401}
402
403impl<'a> StatefulWidget for &Caption<'a> {
404 type State = CaptionState;
405
406 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
407 render_ref(self, area, buf, state);
408 }
409}
410
411impl<'a> StatefulWidget for Caption<'a> {
412 type State = CaptionState;
413
414 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
415 render_ref(&self, area, buf, state);
416 }
417}
418
419fn render_ref(widget: &Caption<'_>, area: Rect, buf: &mut Buffer, state: &mut CaptionState) {
420 state.area = area;
421
422 if widget.navchar.is_some() {
423 state.navchar = widget.navchar;
424 }
425 if widget.hotkey.is_some() {
426 state.hotkey = widget.hotkey;
427 }
428 if let Some(linked) = &widget.linked {
429 state.linked = linked.clone();
430 }
431
432 let mut prepend = String::default();
433 let mut append = String::default();
434
435 let style = if state.linked.is_focused() {
437 if let Some(focus_style) = widget.focus_style {
438 focus_style
439 } else {
440 revert_style(widget.style)
441 }
442 } else {
443 widget.style
444 };
445
446 let mut highlight_style = if let Some(highlight_style) = widget.highlight_style {
447 highlight_style
448 } else {
449 Style::new().underlined()
450 };
451 if let Some(hover_style) = widget.hover_style {
452 if state.mouse.hover.get() {
453 highlight_style = highlight_style.patch(hover_style);
454 }
455 }
456 highlight_style = style.patch(highlight_style);
457
458 let mut hotkey_style = widget.hotkey_style.unwrap_or_default();
459 if let Some(hover_style) = widget.hover_style {
460 if state.mouse.hover.get() {
461 hotkey_style = hotkey_style.patch(hover_style);
462 }
463 }
464 hotkey_style = style.patch(hotkey_style);
465
466 let hotkey_text = if widget.hotkey_policy == HotkeyPolicy::WhenFocused && state.linked.get()
469 || widget.hotkey_policy == HotkeyPolicy::OnHover && state.mouse.hover.get()
470 || widget.hotkey_policy == HotkeyPolicy::Always
471 {
472 widget.hotkey_text.as_ref()
473 } else {
474 ""
475 };
476
477 if !hotkey_text.is_empty() && widget.spacing > 0 {
478 match widget.hotkey_align {
479 HotkeyAlignment::LabelHotkey => {
480 append = " ".repeat(widget.spacing as usize);
481 }
482 HotkeyAlignment::HotkeyLabel => {
483 prepend = " ".repeat(widget.spacing as usize);
484 }
485 }
486 }
487
488 let text_line = match widget.hotkey_align {
489 HotkeyAlignment::LabelHotkey => {
490 if let Some(highlight) = widget.highlight.clone() {
491 Line::from_iter([
492 Span::from(&widget.text[..highlight.start - 1]), Span::from(&widget.text[highlight.start..highlight.end]).style(highlight_style),
494 Span::from(&widget.text[highlight.end..]),
495 Span::from(append),
496 Span::from(hotkey_text).style(hotkey_style),
497 ])
498 } else {
499 Line::from_iter([
500 Span::from(widget.text.as_ref()), Span::from(append),
502 Span::from(hotkey_text).style(hotkey_style),
503 ])
504 }
505 }
506 HotkeyAlignment::HotkeyLabel => {
507 if let Some(highlight) = widget.highlight.clone() {
508 Line::from_iter([
509 Span::from(hotkey_text).style(hotkey_style),
510 Span::from(prepend),
511 Span::from(&widget.text[..highlight.start - 1]), Span::from(&widget.text[highlight.start..highlight.end]).style(highlight_style),
513 Span::from(&widget.text[highlight.end..]),
514 ])
515 } else {
516 Line::from_iter([
517 Span::from(hotkey_text).style(hotkey_style),
518 Span::from(prepend), Span::from(widget.text.as_ref()),
520 ])
521 }
522 }
523 };
524 text_line
525 .alignment(widget.align) .style(style)
527 .render(state.area, buf);
528}
529
530impl Default for CaptionStyle {
531 fn default() -> Self {
532 Self {
533 style: Default::default(),
534 hover: Default::default(),
535 highlight: Default::default(),
536 hotkey: Default::default(),
537 focus: Default::default(),
538 align: Default::default(),
539 hotkey_align: Default::default(),
540 hotkey_policy: Default::default(),
541 spacing: Default::default(),
542 non_exhaustive: NonExhaustive,
543 }
544 }
545}
546
547impl Clone for CaptionState {
548 fn clone(&self) -> Self {
549 Self {
550 area: self.area,
551 navchar: self.navchar,
552 hotkey: self.hotkey,
553 linked: self.linked.clone(),
554 mouse: Default::default(),
555 non_exhaustive: NonExhaustive,
556 }
557 }
558}
559
560impl Default for CaptionState {
561 fn default() -> Self {
562 Self {
563 area: Default::default(),
564 navchar: Default::default(),
565 hotkey: Default::default(),
566 linked: Default::default(),
567 mouse: Default::default(),
568 non_exhaustive: NonExhaustive,
569 }
570 }
571}
572
573impl CaptionState {
574 pub fn new() -> Self {
575 Self::default()
576 }
577
578 pub fn navchar(&self) -> Option<char> {
579 self.navchar
580 }
581
582 pub fn set_navchar(&mut self, navchar: Option<char>) {
583 self.navchar = navchar;
584 }
585
586 pub fn hotkey(&self) -> Option<crossterm::event::KeyEvent> {
587 self.hotkey
588 }
589
590 pub fn set_hotkey(&mut self, hotkey: Option<crossterm::event::KeyEvent>) {
591 self.hotkey = hotkey;
592 }
593
594 pub fn linked(&self) -> FocusFlag {
595 self.linked.clone()
596 }
597
598 pub fn set_linked(&mut self, linked: FocusFlag) {
599 self.linked = linked;
600 }
601}
602
603impl RelocatableState for CaptionState {
604 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
605 self.area = relocate_area(self.area, shift, clip);
606 }
607}
608
609impl<'a> HandleEvent<crossterm::event::Event, &'a Focus, Outcome> for CaptionState {
610 fn handle(&mut self, event: &crossterm::event::Event, focus: &'a Focus) -> Outcome {
611 if let Some(navchar) = self.navchar {
612 if let crossterm::event::Event::Key(crossterm::event::KeyEvent {
613 code: crossterm::event::KeyCode::Char(test),
614 modifiers: crossterm::event::KeyModifiers::ALT,
615 kind: crossterm::event::KeyEventKind::Release,
616 ..
617 }) = event
618 {
619 if navchar == *test {
620 focus.focus(&self.linked);
621 return Outcome::Changed;
622 }
623 }
624 }
625 if let Some(hotkey) = self.hotkey {
626 if let crossterm::event::Event::Key(crossterm::event::KeyEvent {
627 code,
628 modifiers,
629 kind,
630 ..
631 }) = event
632 {
633 if hotkey.code == *code && hotkey.modifiers == *modifiers && hotkey.kind == *kind {
634 focus.focus(&self.linked);
635 return Outcome::Changed;
636 }
637 }
638 }
639
640 match event {
642 ct_event!(mouse any for m) if self.mouse.hover(self.area, m) => {
643 return Outcome::Changed;
644 }
645 ct_event!(mouse down Left for x,y) if self.area.contains((*x, *y).into()) => {
646 focus.focus(&self.linked);
647 return Outcome::Changed;
648 }
649 _ => {}
650 }
651
652 Outcome::Continue
653 }
654}
655
656pub fn handle_events(
661 state: &mut CaptionState,
662 focus: &Focus,
663 event: &crossterm::event::Event,
664) -> Outcome {
665 HandleEvent::handle(state, event, focus)
666}