1use crate::_private::NonExhaustive;
81use crate::layout::GenericLayout;
82use crate::util::revert_style;
83use event::FormOutcome;
84use rat_event::util::MouseFlagsN;
85use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Regular, ct_event};
86use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus};
87use rat_reloc::RelocatableState;
88use ratatui::buffer::Buffer;
89use ratatui::layout::{Alignment, Rect, Size};
90use ratatui::prelude::BlockExt;
91use ratatui::style::Style;
92use ratatui::text::{Line, Span};
93use ratatui::widgets::{Block, StatefulWidget, Widget};
94use std::borrow::Cow;
95use std::cmp::min;
96use std::hash::Hash;
97use std::rc::Rc;
98use unicode_display_width::width as unicode_width;
99
100#[derive(Debug, Clone)]
107pub struct Form<'a, W>
108where
109 W: Eq + Hash + Clone,
110{
111 layout: Option<GenericLayout<W>>,
112
113 style: Style,
114 block: Option<Block<'a>>,
115 nav_style: Option<Style>,
116 title_style: Option<Style>,
117 navigation: bool,
118 next_page: &'a str,
119 prev_page: &'a str,
120 first_page: &'a str,
121 last_page: &'a str,
122
123 auto_label: bool,
124 label_style: Option<Style>,
125 label_alignment: Option<Alignment>,
126}
127
128#[derive(Debug)]
133#[must_use]
134pub struct FormBuffer<'b, W>
135where
136 W: Eq + Hash + Clone,
137{
138 layout: Rc<GenericLayout<W>>,
139
140 page_area: Rect,
141 widget_area: Rect,
142 buffer: &'b mut Buffer,
143
144 auto_label: bool,
145 label_style: Option<Style>,
146 label_alignment: Option<Alignment>,
147}
148
149#[derive(Debug, Clone)]
151pub struct FormStyle {
152 pub style: Style,
154 pub label_style: Option<Style>,
156 pub label_alignment: Option<Alignment>,
158 pub navigation: Option<Style>,
160 pub title: Option<Style>,
162 pub block: Option<Block<'static>>,
164 pub next_page_mark: Option<&'static str>,
166 pub prev_page_mark: Option<&'static str>,
168 pub first_page_mark: Option<&'static str>,
170 pub last_page_mark: Option<&'static str>,
172
173 pub non_exhaustive: NonExhaustive,
174}
175
176#[derive(Debug, Clone)]
178pub struct FormState<W>
179where
180 W: Eq + Hash + Clone,
181{
182 pub layout: Rc<GenericLayout<W>>,
185 pub area: Rect,
188 pub widget_area: Rect,
191 pub prev_area: Rect,
194 pub next_area: Rect,
197
198 pub page: usize,
199
200 pub container: FocusFlag,
203
204 pub mouse: MouseFlagsN,
206
207 pub non_exhaustive: NonExhaustive,
209}
210
211pub(crate) mod event {
212 use rat_event::{ConsumedEvent, Outcome};
213
214 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
216 pub enum FormOutcome {
217 Continue,
219 Unchanged,
222 Changed,
227 Page,
229 }
230
231 impl ConsumedEvent for FormOutcome {
232 fn is_consumed(&self) -> bool {
233 *self != FormOutcome::Continue
234 }
235 }
236
237 impl From<Outcome> for FormOutcome {
238 fn from(value: Outcome) -> Self {
239 match value {
240 Outcome::Continue => FormOutcome::Continue,
241 Outcome::Unchanged => FormOutcome::Unchanged,
242 Outcome::Changed => FormOutcome::Changed,
243 }
244 }
245 }
246
247 impl From<FormOutcome> for Outcome {
248 fn from(value: FormOutcome) -> Self {
249 match value {
250 FormOutcome::Continue => Outcome::Continue,
251 FormOutcome::Unchanged => Outcome::Unchanged,
252 FormOutcome::Changed => Outcome::Changed,
253 FormOutcome::Page => Outcome::Changed,
254 }
255 }
256 }
257}
258
259impl<W> Default for Form<'_, W>
260where
261 W: Eq + Hash + Clone,
262{
263 fn default() -> Self {
264 Self {
265 layout: Default::default(),
266 style: Default::default(),
267 block: Default::default(),
268 nav_style: Default::default(),
269 title_style: Default::default(),
270 navigation: true,
271 next_page: ">>>",
272 prev_page: "<<<",
273 first_page: " [ ",
274 last_page: " ] ",
275 auto_label: true,
276 label_style: Default::default(),
277 label_alignment: Default::default(),
278 }
279 }
280}
281
282impl<'a, W> Form<'a, W>
283where
284 W: Eq + Hash + Clone,
285{
286 pub fn new() -> Self {
288 Self::default()
289 }
290
291 pub fn layout(mut self, layout: GenericLayout<W>) -> Self {
294 self.layout = Some(layout);
295 self
296 }
297
298 pub fn auto_label(mut self, auto: bool) -> Self {
302 self.auto_label = auto;
303 self
304 }
305
306 pub fn style(mut self, style: Style) -> Self {
308 self.style = style;
309 self.block = self.block.map(|v| v.style(style));
310 self
311 }
312
313 pub fn nav_style(mut self, nav_style: Style) -> Self {
315 self.nav_style = Some(nav_style);
316 self
317 }
318
319 pub fn show_navigation(mut self, show: bool) -> Self {
321 self.navigation = show;
322 self
323 }
324
325 pub fn title_style(mut self, title_style: Style) -> Self {
327 self.title_style = Some(title_style);
328 self
329 }
330
331 pub fn block(mut self, block: Block<'a>) -> Self {
333 self.block = Some(block.style(self.style));
334 self
335 }
336
337 pub fn next_page_mark(mut self, txt: &'a str) -> Self {
338 self.next_page = txt;
339 self
340 }
341
342 pub fn prev_page_mark(mut self, txt: &'a str) -> Self {
343 self.prev_page = txt;
344 self
345 }
346
347 pub fn first_page_mark(mut self, txt: &'a str) -> Self {
348 self.first_page = txt;
349 self
350 }
351
352 pub fn last_page_mark(mut self, txt: &'a str) -> Self {
353 self.last_page = txt;
354 self
355 }
356
357 pub fn label_style(mut self, style: Style) -> Self {
359 self.label_style = Some(style);
360 self
361 }
362
363 pub fn label_alignment(mut self, alignment: Alignment) -> Self {
365 self.label_alignment = Some(alignment);
366 self
367 }
368
369 pub fn styles(mut self, styles: FormStyle) -> Self {
371 self.style = styles.style;
372 if let Some(nav) = styles.navigation {
373 self.nav_style = Some(nav);
374 }
375 if let Some(title) = styles.title {
376 self.title_style = Some(title);
377 }
378 if let Some(block) = styles.block {
379 self.block = Some(block);
380 }
381 if let Some(txt) = styles.next_page_mark {
382 self.next_page = txt;
383 }
384 if let Some(txt) = styles.prev_page_mark {
385 self.prev_page = txt;
386 }
387 if let Some(txt) = styles.first_page_mark {
388 self.first_page = txt;
389 }
390 if let Some(txt) = styles.last_page_mark {
391 self.last_page = txt;
392 }
393 self.block = self.block.map(|v| v.style(styles.style));
394
395 if let Some(label) = styles.label_style {
396 self.label_style = Some(label);
397 }
398 if let Some(alignment) = styles.label_alignment {
399 self.label_alignment = Some(alignment);
400 }
401
402 self
403 }
404
405 pub fn layout_size(&self, area: Rect) -> Size {
407 self.block.inner_if_some(area).as_size()
408 }
409
410 pub fn layout_area(&self, area: Rect) -> Rect {
412 if let Some(block) = &self.block {
413 block.inner(area)
414 } else {
415 area
416 }
417 }
418
419 #[allow(clippy::needless_lifetimes)]
422 pub fn into_buffer<'b, 's>(
423 mut self,
424 area: Rect,
425 buf: &'b mut Buffer,
426 state: &'s mut FormState<W>,
427 ) -> FormBuffer<'b, W> {
428 state.area = area;
429 state.widget_area = self.layout_area(area);
430
431 if let Some(layout) = self.layout.take() {
432 state.layout = Rc::new(layout);
433 }
434
435 let page_size = state.layout.page_size();
436 assert!(page_size.height < u16::MAX || page_size.height == u16::MAX && state.page == 0);
437 let page_area = Rect::new(
438 0,
439 (state.page as u16).saturating_mul(page_size.height),
440 page_size.width,
441 page_size.height,
442 );
443
444 if self.navigation {
445 self.render_navigation(area, buf, state);
446 }
447
448 let mut form_buf = FormBuffer {
449 layout: state.layout.clone(),
450 page_area,
451 widget_area: state.widget_area,
452 buffer: buf,
453
454 auto_label: true,
455 label_style: self.label_style,
456 label_alignment: self.label_alignment,
457 };
458 form_buf.render_block();
459 form_buf
460 }
461
462 fn render_navigation(&self, area: Rect, buf: &mut Buffer, state: &mut FormState<W>) {
463 let page_count = state.layout.page_count();
464
465 if !state.layout.is_endless() {
466 if state.page > 0 {
467 state.prev_area =
468 Rect::new(area.x, area.y, unicode_width(self.prev_page) as u16, 1);
469 } else {
470 state.prev_area =
471 Rect::new(area.x, area.y, unicode_width(self.first_page) as u16, 1);
472 }
473 if (state.page + 1) < page_count {
474 let p = unicode_width(self.next_page) as u16;
475 state.next_area = Rect::new(area.x + area.width.saturating_sub(p), area.y, p, 1);
476 } else {
477 let p = unicode_width(self.last_page) as u16;
478 state.next_area = Rect::new(area.x + area.width.saturating_sub(p), area.y, p, 1);
479 }
480 } else {
481 state.prev_area = Default::default();
482 state.next_area = Default::default();
483 }
484
485 let block = if page_count > 1 {
486 let title = format!(" {}/{} ", state.page + 1, page_count);
487 let block = self
488 .block
489 .clone()
490 .unwrap_or_else(|| Block::new().style(self.style))
491 .title_bottom(title)
492 .title_alignment(Alignment::Right);
493 if let Some(title_style) = self.title_style {
494 block.title_style(title_style)
495 } else {
496 block
497 }
498 } else {
499 self.block
500 .clone()
501 .unwrap_or_else(|| Block::new().style(self.style))
502 };
503 block.render(area, buf);
504
505 if !state.layout.is_endless() {
506 let nav_style = self.nav_style.unwrap_or(self.style);
508 if matches!(state.mouse.hover.get(), Some(0)) {
509 buf.set_style(state.prev_area, revert_style(nav_style));
510 } else {
511 buf.set_style(state.prev_area, nav_style);
512 }
513 if state.page > 0 {
514 Span::from(self.prev_page).render(state.prev_area, buf);
515 } else {
516 Span::from(self.first_page).render(state.prev_area, buf);
517 }
518 if matches!(state.mouse.hover.get(), Some(1)) {
519 buf.set_style(state.next_area, revert_style(nav_style));
520 } else {
521 buf.set_style(state.next_area, nav_style);
522 }
523 if (state.page + 1) < page_count {
524 Span::from(self.next_page).render(state.next_area, buf);
525 } else {
526 Span::from(self.last_page).render(state.next_area, buf);
527 }
528 }
529 }
530}
531
532impl<'b, W> FormBuffer<'b, W>
533where
534 W: Eq + Hash + Clone,
535{
536 pub fn is_visible(&self, widget: W) -> bool {
538 if let Some(idx) = self.layout.try_index_of(widget) {
539 self.locate_area(self.layout.widget(idx)).is_some()
540 } else {
541 false
542 }
543 }
544
545 fn render_block(&mut self) {
547 for (idx, block_area) in self.layout.block_area_iter().enumerate() {
548 if let Some(block_area) = self.locate_area(*block_area) {
549 if let Some(block) = self.layout.block(idx) {
550 block.render(block_area, self.buffer);
551 }
552 }
553 }
554 }
555
556 #[inline(always)]
558 pub fn render_label<FN>(&mut self, widget: W, render_fn: FN) -> bool
559 where
560 FN: FnOnce(&Cow<'static, str>, Rect, &mut Buffer),
561 {
562 let Some(idx) = self.layout.try_index_of(widget) else {
563 return false;
564 };
565 let Some(label_area) = self.locate_label(idx) else {
566 return false;
567 };
568 if let Some(label_str) = self.layout.try_label_str(idx) {
569 render_fn(label_str, label_area, self.buffer);
570 } else {
571 render_fn(&Cow::default(), label_area, self.buffer);
572 }
573 true
574 }
575
576 #[inline(always)]
578 pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
579 where
580 FN: FnOnce() -> WW,
581 WW: Widget,
582 {
583 let Some(idx) = self.layout.try_index_of(widget) else {
584 return false;
585 };
586 if self.auto_label {
587 self.render_auto_label(idx);
588 }
589 let Some(widget_area) = self.locate_widget(idx) else {
590 return false;
591 };
592 render_fn().render(widget_area, self.buffer);
593 true
594 }
595
596 #[inline(always)]
598 pub fn render_opt<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
599 where
600 FN: FnOnce() -> Option<WW>,
601 WW: StatefulWidget<State = SS>,
602 SS: RelocatableState,
603 {
604 let Some(idx) = self.layout.try_index_of(widget) else {
605 return false;
606 };
607 if self.auto_label {
608 self.render_auto_label(idx);
609 }
610 let Some(widget_area) = self.locate_widget(idx) else {
611 self.hidden(state);
612 return false;
613 };
614 let widget = render_fn();
615 if let Some(widget) = widget {
616 widget.render(widget_area, self.buffer, state);
617 true
618 } else {
619 self.hidden(state);
620 false
621 }
622 }
623
624 #[inline(always)]
626 pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
627 where
628 FN: FnOnce() -> WW,
629 WW: StatefulWidget<State = SS>,
630 SS: RelocatableState,
631 {
632 let Some(idx) = self.layout.try_index_of(widget) else {
633 return false;
634 };
635 if self.auto_label {
636 self.render_auto_label(idx);
637 }
638 let Some(widget_area) = self.locate_widget(idx) else {
639 self.hidden(state);
640 return false;
641 };
642 let widget = render_fn();
643 widget.render(widget_area, self.buffer, state);
644 true
645 }
646
647 #[inline(always)]
651 #[allow(clippy::question_mark)]
652 pub fn render2<FN, WW, SS, R>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> Option<R>
653 where
654 FN: FnOnce() -> (WW, R),
655 WW: StatefulWidget<State = SS>,
656 SS: RelocatableState,
657 {
658 let Some(idx) = self.layout.try_index_of(widget) else {
659 return None;
660 };
661 if self.auto_label {
662 self.render_auto_label(idx);
663 }
664 let Some(widget_area) = self.locate_widget(idx) else {
665 self.hidden(state);
666 return None;
667 };
668 let (widget, remainder) = render_fn();
669 widget.render(widget_area, self.buffer, state);
670
671 Some(remainder)
672 }
673
674 pub fn buffer(&mut self) -> &mut Buffer {
676 self.buffer
677 }
678
679 #[inline(always)]
681 fn render_auto_label(&mut self, idx: usize) -> bool {
682 let Some(label_area) = self.locate_label(idx) else {
683 return false;
684 };
685 let Some(label_str) = self.layout.try_label_str(idx) else {
686 return false;
687 };
688 let mut label = Line::from(label_str.as_ref());
689 if let Some(style) = self.label_style {
690 label = label.style(style)
691 };
692 if let Some(align) = self.label_alignment {
693 label = label.alignment(align);
694 }
695 label.render(label_area, self.buffer);
696
697 true
698 }
699
700 #[inline]
704 fn locate_widget(&self, idx: usize) -> Option<Rect> {
705 self.locate_area(self.layout.widget(idx))
706 }
707
708 #[inline]
712 fn locate_label(&self, idx: usize) -> Option<Rect> {
713 self.locate_area(self.layout.label(idx))
714 }
715
716 #[inline]
718 fn locate_area(&self, area: Rect) -> Option<Rect> {
719 let area = self.page_area.intersection(area);
721 if !area.is_empty() {
722 let located = Rect::new(
723 area.x - self.page_area.x + self.widget_area.x,
724 area.y - self.page_area.y + self.widget_area.y,
725 area.width,
726 area.height,
727 );
728 let located = self.widget_area.intersection(located);
730 if !located.is_empty() {
731 Some(located)
732 } else {
733 None
734 }
735 } else {
736 None
737 }
738 }
739
740 fn hidden<S>(&self, state: &mut S)
743 where
744 S: RelocatableState,
745 {
746 state.relocate((0, 0), Rect::default())
747 }
748}
749
750impl Default for FormStyle {
751 fn default() -> Self {
752 Self {
753 style: Default::default(),
754 label_style: None,
755 label_alignment: None,
756 navigation: None,
757 title: None,
758 block: None,
759 next_page_mark: None,
760 prev_page_mark: None,
761 first_page_mark: None,
762 last_page_mark: None,
763 non_exhaustive: NonExhaustive,
764 }
765 }
766}
767
768impl<W> Default for FormState<W>
769where
770 W: Eq + Hash + Clone,
771{
772 fn default() -> Self {
773 Self {
774 layout: Default::default(),
775 area: Default::default(),
776 widget_area: Default::default(),
777 prev_area: Default::default(),
778 next_area: Default::default(),
779 page: 0,
780 container: Default::default(),
781 mouse: Default::default(),
782 non_exhaustive: NonExhaustive,
783 }
784 }
785}
786
787impl<W> HasFocus for FormState<W>
788where
789 W: Eq + Hash + Clone,
790{
791 fn build(&self, _builder: &mut FocusBuilder) {
792 }
794
795 fn focus(&self) -> FocusFlag {
796 self.container.clone()
797 }
798
799 fn area(&self) -> Rect {
800 self.area
801 }
802}
803
804impl<W> FormState<W>
805where
806 W: Eq + Hash + Clone,
807{
808 pub fn new() -> Self {
809 Self::default()
810 }
811
812 pub fn clear(&mut self) {
814 self.layout = Default::default();
815 self.page = 0;
816 }
817
818 pub fn valid_layout(&self, size: Size) -> bool {
820 !self.layout.size_changed(size) && !self.layout.is_empty()
821 }
822
823 pub fn set_layout(&mut self, layout: GenericLayout<W>) {
825 self.layout = Rc::new(layout);
826 }
827
828 pub fn layout(&self) -> Rc<GenericLayout<W>> {
830 self.layout.clone()
831 }
832
833 pub fn show(&mut self, widget: W) {
837 let page = self.layout.page_of(widget).unwrap_or_default();
838 self.set_page(page);
839 }
840
841 pub fn page_count(&self) -> usize {
843 self.layout.page_count()
844 }
845
846 pub fn first(&self, page: usize) -> Option<W> {
848 self.layout.first(page)
849 }
850
851 pub fn page_of(&self, widget: W) -> Option<usize> {
853 self.layout.page_of(widget)
854 }
855
856 pub fn set_page(&mut self, page: usize) -> bool {
858 let old_page = self.page;
859 self.page = min(page, self.page_count().saturating_sub(1));
860 old_page != self.page
861 }
862
863 pub fn page(&self) -> usize {
865 self.page
866 }
867
868 pub fn next_page(&mut self) -> bool {
870 let old_page = self.page;
871
872 if self.page + 1 == self.page_count() {
873 } else if self.page + 1 > self.page_count() {
875 self.page = self.page_count().saturating_sub(1);
876 } else {
877 self.page += 1;
878 }
879
880 old_page != self.page
881 }
882
883 pub fn prev_page(&mut self) -> bool {
885 if self.page >= 1 {
886 self.page -= 1;
887 true
888 } else if self.page > 0 {
889 self.page = 0;
890 true
891 } else {
892 false
893 }
894 }
895}
896
897impl FormState<usize> {
898 pub fn focus_first(&self, focus: &Focus) -> bool {
901 if let Some(w) = self.first(self.page) {
902 focus.by_widget_id(w);
903 true
904 } else {
905 false
906 }
907 }
908
909 pub fn show_focused(&mut self, focus: &Focus) -> bool {
913 let Some(focused) = focus.focused() else {
914 return false;
915 };
916 let focused = focused.widget_id();
917 let page = self.layout.page_of(focused);
918 if let Some(page) = page {
919 self.set_page(page);
920 true
921 } else {
922 false
923 }
924 }
925}
926
927impl FormState<FocusFlag> {
928 pub fn focus_first(&self, focus: &Focus) -> bool {
930 if let Some(w) = self.first(self.page) {
931 focus.focus(&w);
932 true
933 } else {
934 false
935 }
936 }
937
938 pub fn show_focused(&mut self, focus: &Focus) -> bool {
941 let Some(focused) = focus.focused() else {
942 return false;
943 };
944 let page = self.layout.page_of(focused);
945 if let Some(page) = page {
946 self.set_page(page);
947 true
948 } else {
949 false
950 }
951 }
952}
953
954impl<W> HandleEvent<crossterm::event::Event, Regular, FormOutcome> for FormState<W>
955where
956 W: Eq + Hash + Clone,
957{
958 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> FormOutcome {
959 let r = if self.container.is_focused() && !self.layout.is_endless() {
960 match event {
961 ct_event!(keycode press ALT-PageUp) => {
962 if self.prev_page() {
963 FormOutcome::Page
964 } else {
965 FormOutcome::Continue
966 }
967 }
968 ct_event!(keycode press ALT-PageDown) => {
969 if self.next_page() {
970 FormOutcome::Page
971 } else {
972 FormOutcome::Continue
973 }
974 }
975 _ => FormOutcome::Continue,
976 }
977 } else {
978 FormOutcome::Continue
979 };
980
981 r.or_else(|| self.handle(event, MouseOnly))
982 }
983}
984
985impl<W> HandleEvent<crossterm::event::Event, MouseOnly, FormOutcome> for FormState<W>
986where
987 W: Eq + Hash + Clone,
988{
989 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> FormOutcome {
990 if !self.layout.is_endless() {
991 match event {
992 ct_event!(mouse down Left for x,y) if self.prev_area.contains((*x, *y).into()) => {
993 if self.prev_page() {
994 FormOutcome::Page
995 } else {
996 FormOutcome::Unchanged
997 }
998 }
999 ct_event!(mouse down Left for x,y) if self.next_area.contains((*x, *y).into()) => {
1000 if self.next_page() {
1001 FormOutcome::Page
1002 } else {
1003 FormOutcome::Unchanged
1004 }
1005 }
1006 ct_event!(scroll down for x,y) => {
1007 if self.area.contains((*x, *y).into()) {
1008 if self.next_page() {
1009 FormOutcome::Page
1010 } else {
1011 FormOutcome::Continue
1012 }
1013 } else {
1014 FormOutcome::Continue
1015 }
1016 }
1017 ct_event!(scroll up for x,y) => {
1018 if self.area.contains((*x, *y).into()) {
1019 if self.prev_page() {
1020 FormOutcome::Page
1021 } else {
1022 FormOutcome::Continue
1023 }
1024 } else {
1025 FormOutcome::Continue
1026 }
1027 }
1028 ct_event!(mouse any for m)
1029 if self.mouse.hover(&[self.prev_area, self.next_area], m) =>
1030 {
1031 FormOutcome::Changed
1032 }
1033 _ => FormOutcome::Continue,
1034 }
1035 } else {
1036 FormOutcome::Continue
1037 }
1038 }
1039}