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