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 && self.navigation
419 {
420 block.inner(area)
421 } else {
422 area
423 }
424 }
425
426 #[allow(clippy::needless_lifetimes)]
429 pub fn into_buffer<'b, 's>(
430 mut self,
431 area: Rect,
432 buf: &'b mut Buffer,
433 state: &'s mut FormState<W>,
434 ) -> FormBuffer<'b, W> {
435 state.area = area;
436 state.widget_area = self.layout_area(area);
437
438 if let Some(layout) = self.layout.take() {
439 state.layout = Rc::new(layout);
440 }
441
442 let page_size = state.layout.page_size();
443 assert!(page_size.height < u16::MAX || page_size.height == u16::MAX && state.page == 0);
444 let page_area = Rect::new(
445 0,
446 (state.page as u16).saturating_mul(page_size.height),
447 page_size.width,
448 page_size.height,
449 );
450
451 if self.navigation {
452 self.render_navigation(area, buf, state);
453 } else {
454 buf.set_style(area, self.style);
455 }
456
457 let mut form_buf = FormBuffer {
458 layout: state.layout.clone(),
459 page_area,
460 widget_area: state.widget_area,
461 buffer: buf,
462
463 auto_label: true,
464 label_style: self.label_style,
465 label_alignment: self.label_alignment,
466 };
467 form_buf.render_block();
468 form_buf
469 }
470
471 fn render_navigation(&self, area: Rect, buf: &mut Buffer, state: &mut FormState<W>) {
472 let page_count = state.layout.page_count();
473
474 if !state.layout.is_endless() {
475 if state.page > 0 {
476 state.prev_area =
477 Rect::new(area.x, area.y, unicode_width(self.prev_page) as u16, 1);
478 } else {
479 state.prev_area =
480 Rect::new(area.x, area.y, unicode_width(self.first_page) as u16, 1);
481 }
482 if (state.page + 1) < page_count {
483 let p = unicode_width(self.next_page) as u16;
484 state.next_area = Rect::new(area.x + area.width.saturating_sub(p), area.y, p, 1);
485 } else {
486 let p = unicode_width(self.last_page) as u16;
487 state.next_area = Rect::new(area.x + area.width.saturating_sub(p), area.y, p, 1);
488 }
489 } else {
490 state.prev_area = Default::default();
491 state.next_area = Default::default();
492 }
493
494 let block = if page_count > 1 {
495 let title = format!(" {}/{} ", state.page + 1, page_count);
496 let block = self
497 .block
498 .clone()
499 .unwrap_or_else(|| Block::new().style(self.style))
500 .title_bottom(title)
501 .title_alignment(Alignment::Right);
502 if let Some(title_style) = self.title_style {
503 block.title_style(title_style)
504 } else {
505 block
506 }
507 } else {
508 self.block
509 .clone()
510 .unwrap_or_else(|| Block::new().style(self.style))
511 };
512 block.render(area, buf);
513
514 if !state.layout.is_endless() {
515 let nav_style = self.nav_style.unwrap_or(self.style);
517 if matches!(state.mouse.hover.get(), Some(0)) {
518 buf.set_style(state.prev_area, revert_style(nav_style));
519 } else {
520 buf.set_style(state.prev_area, nav_style);
521 }
522 if state.page > 0 {
523 Span::from(self.prev_page).render(state.prev_area, buf);
524 } else {
525 Span::from(self.first_page).render(state.prev_area, buf);
526 }
527 if matches!(state.mouse.hover.get(), Some(1)) {
528 buf.set_style(state.next_area, revert_style(nav_style));
529 } else {
530 buf.set_style(state.next_area, nav_style);
531 }
532 if (state.page + 1) < page_count {
533 Span::from(self.next_page).render(state.next_area, buf);
534 } else {
535 Span::from(self.last_page).render(state.next_area, buf);
536 }
537 }
538 }
539}
540
541impl<'b, W> FormBuffer<'b, W>
542where
543 W: Eq + Hash + Clone,
544{
545 pub fn is_visible(&self, widget: W) -> bool {
547 if let Some(idx) = self.layout.try_index_of(widget) {
548 self.locate_area(self.layout.widget(idx)).is_some()
549 } else {
550 false
551 }
552 }
553
554 fn render_block(&mut self) {
556 for (idx, block_area) in self.layout.block_area_iter().enumerate() {
557 if let Some(block_area) = self.locate_area(*block_area) {
558 if let Some(block) = self.layout.block(idx) {
559 block.render(block_area, self.buffer);
560 }
561 }
562 }
563 }
564
565 #[inline(always)]
567 pub fn render_label<FN>(&mut self, widget: W, render_fn: FN) -> bool
568 where
569 FN: FnOnce(&Cow<'static, str>, Rect, &mut Buffer),
570 {
571 let Some(idx) = self.layout.try_index_of(widget) else {
572 return false;
573 };
574 let Some(label_area) = self.locate_area(self.layout.label(idx)) else {
575 return false;
576 };
577 if let Some(label_str) = self.layout.try_label_str(idx) {
578 render_fn(label_str, label_area, self.buffer);
579 } else {
580 render_fn(&Cow::default(), label_area, self.buffer);
581 }
582 true
583 }
584
585 #[inline(always)]
587 pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
588 where
589 FN: FnOnce() -> WW,
590 WW: Widget,
591 {
592 let Some(idx) = self.layout.try_index_of(widget) else {
593 return false;
594 };
595 if self.auto_label {
596 self.render_auto_label(idx);
597 }
598
599 let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
600 return false;
601 };
602 render_fn().render(widget_area, self.buffer);
603 true
604 }
605
606 #[inline(always)]
608 pub fn render_opt<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
609 where
610 FN: FnOnce() -> Option<WW>,
611 WW: StatefulWidget<State = SS>,
612 SS: RelocatableState,
613 {
614 let Some(idx) = self.layout.try_index_of(widget) else {
615 return false;
616 };
617 if self.auto_label {
618 self.render_auto_label(idx);
619 }
620 let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
621 state.relocate_hidden();
622 return false;
623 };
624 let widget = render_fn();
625 if let Some(widget) = widget {
626 widget.render(widget_area, self.buffer, state);
627 true
628 } else {
629 state.relocate_hidden();
630 false
631 }
632 }
633
634 #[inline(always)]
636 pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
637 where
638 FN: FnOnce() -> WW,
639 WW: StatefulWidget<State = SS>,
640 SS: RelocatableState,
641 {
642 let Some(idx) = self.layout.try_index_of(widget) else {
643 return false;
644 };
645 if self.auto_label {
646 self.render_auto_label(idx);
647 }
648 let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
649 state.relocate_hidden();
650 return false;
651 };
652 let widget = render_fn();
653 widget.render(widget_area, self.buffer, state);
654 true
655 }
656
657 #[inline(always)]
661 #[allow(clippy::question_mark)]
662 pub fn render2<FN, WW, SS, R>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> Option<R>
663 where
664 FN: FnOnce() -> (WW, R),
665 WW: StatefulWidget<State = SS>,
666 SS: RelocatableState,
667 {
668 let Some(idx) = self.layout.try_index_of(widget) else {
669 return None;
670 };
671 if self.auto_label {
672 self.render_auto_label(idx);
673 }
674 let Some(widget_area) = self.locate_area(self.layout.widget(idx)) else {
675 state.relocate_hidden();
676 return None;
677 };
678 let (widget, remainder) = render_fn();
679 widget.render(widget_area, self.buffer, state);
680
681 Some(remainder)
682 }
683
684 pub fn buffer(&mut self) -> &mut Buffer {
686 self.buffer
687 }
688
689 #[inline(always)]
691 fn render_auto_label(&mut self, idx: usize) -> bool {
692 let Some(label_area) = self.locate_area(self.layout.label(idx)) else {
693 return false;
694 };
695 let Some(label_str) = self.layout.try_label_str(idx) else {
696 return false;
697 };
698 let mut label = Line::from(label_str.as_ref());
699 if let Some(style) = self.label_style {
700 label = label.style(style)
701 };
702 if let Some(align) = self.label_alignment {
703 label = label.alignment(align);
704 }
705 label.render(label_area, self.buffer);
706
707 true
708 }
709
710 pub fn locate_widget(&self, widget: W) -> Option<Rect> {
712 let Some(idx) = self.layout.try_index_of(widget) else {
713 return None;
714 };
715 self.locate_area(self.layout.widget(idx))
716 }
717
718 pub fn locate_label(&self, widget: W) -> Option<Rect> {
720 let Some(idx) = self.layout.try_index_of(widget) else {
721 return None;
722 };
723 self.locate_area(self.layout.label(idx))
724 }
725
726 #[inline]
728 pub fn locate_area(&self, area: Rect) -> Option<Rect> {
729 let area = self.page_area.intersection(area);
731 if self.page_area.intersects(area) {
732 let located = Rect::new(
733 area.x - self.page_area.x + self.widget_area.x,
734 area.y - self.page_area.y + self.widget_area.y,
735 area.width,
736 area.height,
737 );
738 let located = self.widget_area.intersection(located);
740 if self.widget_area.intersects(located) {
741 Some(located)
742 } else {
743 None
744 }
745 } else {
746 None
747 }
748 }
749}
750
751impl Default for FormStyle {
752 fn default() -> Self {
753 Self {
754 style: Default::default(),
755 label_style: None,
756 label_alignment: None,
757 navigation: None,
758 show_navigation: None,
759 title: None,
760 block: None,
761 next_page_mark: None,
762 prev_page_mark: None,
763 first_page_mark: None,
764 last_page_mark: None,
765 non_exhaustive: NonExhaustive,
766 }
767 }
768}
769
770impl<W> Default for FormState<W>
771where
772 W: Eq + Hash + Clone,
773{
774 fn default() -> Self {
775 Self {
776 layout: Default::default(),
777 area: Default::default(),
778 widget_area: Default::default(),
779 prev_area: Default::default(),
780 next_area: Default::default(),
781 page: 0,
782 container: Default::default(),
783 mouse: Default::default(),
784 non_exhaustive: NonExhaustive,
785 }
786 }
787}
788
789impl<W> HasFocus for FormState<W>
790where
791 W: Eq + Hash + Clone,
792{
793 fn build(&self, _builder: &mut FocusBuilder) {
794 }
796
797 fn focus(&self) -> FocusFlag {
798 self.container.clone()
799 }
800
801 fn area(&self) -> Rect {
802 self.area
803 }
804}
805
806impl<W> FormState<W>
807where
808 W: Eq + Hash + Clone,
809{
810 pub fn new() -> Self {
811 Self::default()
812 }
813
814 pub fn clear(&mut self) {
816 self.layout = Default::default();
817 self.page = 0;
818 }
819
820 pub fn valid_layout(&self, size: Size) -> bool {
822 !self.layout.size_changed(size) && !self.layout.is_empty()
823 }
824
825 pub fn set_layout(&mut self, layout: GenericLayout<W>) {
827 self.layout = Rc::new(layout);
828 }
829
830 pub fn layout(&self) -> Rc<GenericLayout<W>> {
832 self.layout.clone()
833 }
834
835 pub fn show(&mut self, widget: W) {
839 let page = self.layout.page_of(widget).unwrap_or_default();
840 self.set_page(page);
841 }
842
843 pub fn page_count(&self) -> usize {
845 self.layout.page_count()
846 }
847
848 pub fn first(&self, page: usize) -> Option<W> {
850 self.layout.first(page)
851 }
852
853 pub fn page_of(&self, widget: W) -> Option<usize> {
855 self.layout.page_of(widget)
856 }
857
858 pub fn set_page(&mut self, page: usize) -> bool {
860 let old_page = self.page;
861 self.page = min(page, self.page_count().saturating_sub(1));
862 old_page != self.page
863 }
864
865 pub fn page(&self) -> usize {
867 self.page
868 }
869
870 pub fn next_page(&mut self) -> bool {
872 let old_page = self.page;
873
874 if self.page + 1 == self.page_count() {
875 } else if self.page + 1 > self.page_count() {
877 self.page = self.page_count().saturating_sub(1);
878 } else {
879 self.page += 1;
880 }
881
882 old_page != self.page
883 }
884
885 pub fn prev_page(&mut self) -> bool {
887 if self.page >= 1 {
888 self.page -= 1;
889 true
890 } else if self.page > 0 {
891 self.page = 0;
892 true
893 } else {
894 false
895 }
896 }
897}
898
899impl FormState<usize> {
900 pub fn focus_first(&self, focus: &Focus) -> bool {
903 if let Some(w) = self.first(self.page) {
904 focus.by_widget_id(w);
905 true
906 } else {
907 false
908 }
909 }
910
911 pub fn show_focused(&mut self, focus: &Focus) -> bool {
915 let Some(focused) = focus.focused() else {
916 return false;
917 };
918 let focused = focused.widget_id();
919 let page = self.layout.page_of(focused);
920 if let Some(page) = page {
921 self.set_page(page);
922 true
923 } else {
924 false
925 }
926 }
927}
928
929impl FormState<FocusFlag> {
930 pub fn focus_first(&self, focus: &Focus) -> bool {
932 if let Some(w) = self.first(self.page) {
933 focus.focus(&w);
934 true
935 } else {
936 false
937 }
938 }
939
940 pub fn show_focused(&mut self, focus: &Focus) -> bool {
943 let Some(focused) = focus.focused() else {
944 return false;
945 };
946 let page = self.layout.page_of(focused);
947 if let Some(page) = page {
948 self.set_page(page);
949 true
950 } else {
951 false
952 }
953 }
954}
955
956impl<W> HandleEvent<crossterm::event::Event, Regular, FormOutcome> for FormState<W>
957where
958 W: Eq + Hash + Clone,
959{
960 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> FormOutcome {
961 let r = if self.container.is_focused() && !self.layout.is_endless() {
962 match event {
963 ct_event!(keycode press ALT-PageUp) => {
964 if self.prev_page() {
965 FormOutcome::Page
966 } else {
967 FormOutcome::Continue
968 }
969 }
970 ct_event!(keycode press ALT-PageDown) => {
971 if self.next_page() {
972 FormOutcome::Page
973 } else {
974 FormOutcome::Continue
975 }
976 }
977 _ => FormOutcome::Continue,
978 }
979 } else {
980 FormOutcome::Continue
981 };
982
983 r.or_else(|| self.handle(event, MouseOnly))
984 }
985}
986
987impl<W> HandleEvent<crossterm::event::Event, MouseOnly, FormOutcome> for FormState<W>
988where
989 W: Eq + Hash + Clone,
990{
991 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> FormOutcome {
992 if !self.layout.is_endless() {
993 match event {
994 ct_event!(mouse down Left for x,y) if self.prev_area.contains((*x, *y).into()) => {
995 if self.prev_page() {
996 FormOutcome::Page
997 } else {
998 FormOutcome::Unchanged
999 }
1000 }
1001 ct_event!(mouse down Left for x,y) if self.next_area.contains((*x, *y).into()) => {
1002 if self.next_page() {
1003 FormOutcome::Page
1004 } else {
1005 FormOutcome::Unchanged
1006 }
1007 }
1008 ct_event!(scroll down for x,y) => {
1009 if self.area.contains((*x, *y).into()) {
1010 if self.next_page() {
1011 FormOutcome::Page
1012 } else {
1013 FormOutcome::Continue
1014 }
1015 } else {
1016 FormOutcome::Continue
1017 }
1018 }
1019 ct_event!(scroll up for x,y) => {
1020 if self.area.contains((*x, *y).into()) {
1021 if self.prev_page() {
1022 FormOutcome::Page
1023 } else {
1024 FormOutcome::Continue
1025 }
1026 } else {
1027 FormOutcome::Continue
1028 }
1029 }
1030 ct_event!(mouse any for m)
1031 if self.mouse.hover(&[self.prev_area, self.next_area], m) =>
1032 {
1033 FormOutcome::Changed
1034 }
1035 _ => FormOutcome::Continue,
1036 }
1037 } else {
1038 FormOutcome::Continue
1039 }
1040 }
1041}