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