1pub mod presets;
2use colorgrad::Gradient;
3use derive_builder::Builder;
4use getset::{Getters, Setters};
5use ratatui::{
6 layout::Margin,
7 prelude::{Alignment, Buffer, Rect},
8 style::Color,
9 text::Line,
10 widgets::{Padding, Widget, WidgetRef},
11};
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14#[cfg(feature = "all")]
15pub struct Rule {
25 pub gradient: Option<Box<dyn Gradient>>,
26 pub symbol_set: Set,
27 pub orientation: Orientation,
28 pub padding: Padding,
29 pub vertical_alignment: VerticalAlignment,
30 pub horizontal_alignment: Alignment,
31 pub extra_rep_1: usize,
32 pub extra_rep_2: usize,
33 pub bg: Bg,
34 pub area_margin: Margin,
35}
36pub enum Bg {
37 None,
38 Solid(Color),
39 Gradient,
40 GradientCustom(Box<dyn Gradient>),
41}
42#[macro_export]
43macro_rules! create_segment {
44 ($set:expr, $p_1:expr, $p_2:expr, $base_area:expr, $orientation:expr, $h_alignment:expr, $v_alignment:expr, $extra_rep_1:expr, $extra_rep_2:expr) => {{
45 let rep_count: f32 = ($base_area / 2.0) - 1.0;
46 let seg1 = $set.rep_1.to_string().repeat(
47 (rep_count.floor() as usize)
48 .saturating_sub($p_1)
49 .saturating_add($extra_rep_1),
50 );
51 let seg2 = $set.rep_2.to_string().repeat(
52 (rep_count.round() as usize)
53 .saturating_sub($p_2 + 1)
54 .saturating_add($extra_rep_2),
55 );
56 let mut ln = String::with_capacity(
57 $p_1 + $p_2
58 + 1
59 + seg1.len()
60 + 1
61 + seg2.len()
62 + 5,
63 );
64 ln.push_str(&String::from(" ").repeat(
65 match $orientation {
66 Orientation::Horizontal => {
67 match $h_alignment {
68 Alignment::Left => 0,
69 Alignment::Center => $p_1,
70
71 Alignment::Right => {
72 $p_1.saturating_add($p_2)
73 }
74 }
75 }
76 Orientation::Vertical => match $v_alignment
77 {
78 VerticalAlignment::Top => 0,
79 VerticalAlignment::Center => $p_1,
80
81 VerticalAlignment::Bottom => {
82 $p_1.saturating_add($p_2)
83 }
84 },
85 } as usize,
86 ));
87 ln.push($set.start);
88 ln.push_str(&seg1);
89 ln.push($set.center);
90 ln.push_str(&seg2);
91 ln.push($set.end);
92 ln.push_str(&String::from(" ").repeat(
93 match $orientation {
94 Orientation::Horizontal => {
95 match $h_alignment {
96 Alignment::Left => {
97 $p_2.saturating_add($p_1)
98 }
99 Alignment::Center => $p_2,
100 Alignment::Right => 0,
101 }
102 }
103 Orientation::Vertical => match $v_alignment
104 {
105 VerticalAlignment::Top => {
106 $p_1.saturating_add($p_2)
107 }
108 VerticalAlignment::Center => $p_2,
109 VerticalAlignment::Bottom => 0,
110 },
111 } as usize,
112 ));
113 ln
114 }};
115}
116#[cfg_attr(
130 feature = "serde",
131 derive(Serialize, Deserialize)
132)]
133#[derive(Builder, Getters, Setters, Debug, Clone)]
134pub struct Set {
135 #[builder(default = "'─'")]
136 pub start: char,
137 #[builder(default = "'─'")]
138 pub end: char,
139 #[builder(default = "'─'")]
140 pub rep_1: char,
141 #[builder(default = "'─'")]
142 pub rep_2: char,
143 #[builder(default = "'─'")]
144 pub center: char,
145}
146#[cfg_attr(
148 feature = "serde",
149 derive(Serialize, Deserialize)
150)]
151#[derive(Clone, Debug, PartialEq, Hash)]
152pub enum Orientation {
153 Vertical,
154 Horizontal,
155}
156#[cfg_attr(
158 feature = "serde",
159 derive(Serialize, Deserialize)
160)]
161#[derive(Clone, Debug, PartialEq, Hash)]
162pub enum VerticalAlignment {
163 Top,
164 Center,
165 Bottom,
166}
167#[macro_export]
178macro_rules! generate_gradient_text {
179 ($txt:expr, $gr:expr) => {{
180 use ratatui::prelude::{Color, Style};
181 let mut ln: Line = $txt.into();
182 ln.spans = create_raw_spans!(ln.spans[0].content);
183 let mut new_text = Vec::new();
184 for (s, c) in ln
185 .spans
186 .clone()
187 .into_iter()
188 .zip($gr.colors(ln.width()))
189 {
190 new_text.push(s.style(Style::new().fg(
191 Color::Rgb(
192 (c.r * 255.0) as u8,
193 (c.g * 255.0) as u8,
194 (c.b * 255.0) as u8,
195 ),
196 )));
197 }
198 new_text
199 }};
200 ($txt:expr, $gr:expr, $bgtype:expr) => {{
201 use ratatui::prelude::{Color, Style};
202 let mut ln: Line = $txt.into();
203 ln.spans = create_raw_spans!(ln.spans[0].content);
204 let mut new_text = Vec::new();
205 match $bgtype {
206 Bg::GradientCustom(grad) => {
207 for (s, (c, c2)) in
208 ln.spans.clone().into_iter().zip(
209 $gr.colors(ln.width())
210 .into_iter()
211 .zip(grad.colors(ln.width())),
212 )
213 {
214 new_text.push(
215 s.style(
216 Style::new()
217 .fg(Color::Rgb(
218 (c.r * 255.0) as u8,
219 (c.g * 255.0) as u8,
220 (c.b * 255.0) as u8,
221 ))
222 .bg(Color::Rgb(
223 (c2.r * 255.0) as u8,
224 (c2.g * 255.0) as u8,
225 (c2.b * 255.0) as u8,
226 )),
227 ),
228 );
229 }
230 }
231 _ => {
232 for (s, c) in ln
233 .spans
234 .clone()
235 .into_iter()
236 .zip($gr.colors(ln.width()))
237 {
238 let c = Color::Rgb(
239 (c.r * 255.0) as u8,
240 (c.g * 255.0) as u8,
241 (c.b * 255.0) as u8,
242 );
243 new_text.push(s.style(
244 Style::new().fg(c).bg(
245 match $bgtype {
246 Bg::Solid(color) => *color,
247 Bg::Gradient => c,
248 _ => c,
249 },
250 ),
251 ));
252 }
253 }
254 }
255 new_text
256 }};
257}
258#[cfg(feature = "all")]
259impl Default for Rule {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264#[cfg(feature = "all")]
265impl Rule {
266 pub fn new() -> Self {
269 Self {
270 gradient: None,
271 symbol_set: Set {
272 start: '─',
273 end: '─',
274 center: '─',
275 rep_1: '─',
276 rep_2: '─',
277 },
278 padding: Padding::new(0, 0, 0, 0),
279 orientation: Orientation::Horizontal,
280 horizontal_alignment: Alignment::Center,
281 vertical_alignment: VerticalAlignment::Center,
282 bg: Bg::None,
283 area_margin: Margin::new(1, 1),
284 extra_rep_1: 0,
285 extra_rep_2: 0,
286 }
287 }
288 pub fn area_margin(mut self, margin: Margin) -> Self {
289 self.area_margin = margin;
290 self
291 }
292 pub fn bg_solid(mut self, c: Color) -> Self {
294 self.bg = Bg::Solid(c);
295 self
296 }
297 pub fn bg_same_gradient(mut self) -> Self {
299 self.bg = Bg::Gradient;
300 self
301 }
302 pub fn extra_rep_1(mut self, rep: usize) -> Self {
303 self.extra_rep_1 = rep;
304 self
305 }
306 pub fn extra_rep_2(mut self, rep: usize) -> Self {
307 self.extra_rep_2 = rep;
308 self
309 }
310 pub fn extra_rep(
311 mut self,
312 rep_1: usize,
313 rep_2: usize,
314 ) -> Self {
315 self.extra_rep_1 = rep_1;
316 self.extra_rep_2 = rep_2;
317 self
318 }
319 pub fn bg_gradient<G: Gradient + 'static>(
321 mut self,
322 g: G,
323 ) -> Self {
324 self.bg = Bg::GradientCustom(Box::<G>::new(g));
325 self
326 }
327 pub fn bg(mut self, bg: Bg) -> Self {
328 self.bg = bg;
329 self
330 }
331 pub fn new_vertical() -> Self {
333 Self::new().vertical()
334 }
335 pub fn from_set(set: Set) -> Self {
342 Self::new().with_set(set)
343 }
344 pub fn new_with_gradient<G: Gradient + 'static>(
350 gradient: G,
351 ) -> Self {
352 Self::new().with_gradient(gradient)
353 }
354 pub fn with_gradient<G: Gradient + 'static>(
360 mut self,
361 gradient: G,
362 ) -> Self {
363 self.gradient = Some(Box::<G>::new(gradient));
364 self
365 }
366 pub fn horizontal_padding(
368 mut self,
369 padding: u16,
370 ) -> Self {
371 self.padding.right = padding;
372 self.padding.left = padding;
373 self
374 }
375 pub fn vertical_padding(
377 mut self,
378 padding: u16,
379 ) -> Self {
380 self.padding.bottom = padding;
381 self.padding.top = padding;
382 self
383 }
384 pub fn right_padding(mut self, padding: u16) -> Self {
386 self.padding.right = padding;
387 self
388 }
389
390 pub fn left_padding(mut self, padding: u16) -> Self {
392 self.padding.left = padding;
393 self
394 }
395
396 pub fn top_padding(mut self, padding: u16) -> Self {
398 self.padding.top = padding;
399 self
400 }
401
402 pub fn bottom_padding(mut self, padding: u16) -> Self {
404 self.padding.bottom = padding;
405 self
406 }
407 pub fn with_set(mut self, set: Set) -> Self {
409 self = self
410 .end(set.end)
411 .start(set.start)
412 .rep_2(set.rep_2)
413 .rep_1(set.rep_1)
414 .center(set.center);
415 self
416 }
417 pub fn horizontal(mut self) -> Self {
419 self.orientation = Orientation::Horizontal;
420 self
421 }
422 pub fn vertical(mut self) -> Self {
424 self.orientation = Orientation::Vertical;
425 self
426 }
427 pub fn rep_2(mut self, symb: char) -> Self {
433 self.symbol_set.rep_2 = symb;
434 self
435 }
436 pub fn rep_1(mut self, symb: char) -> Self {
442 self.symbol_set.rep_1 = symb;
443 self
444 }
445 pub fn start(mut self, symb: char) -> Self {
451 self.symbol_set.start = symb;
452 self
453 }
454 pub fn end(mut self, symb: char) -> Self {
460 self.symbol_set.end = symb;
461 self
462 }
463 pub fn center(mut self, symb: char) -> Self {
469 self.symbol_set.center = symb;
470 self
471 }
472 pub fn main_symbol(mut self, symb: char) -> Self {
474 self = self.rep_1(symb).rep_2(symb);
475 self
476 }
477 pub fn padding(mut self, padding: Padding) -> Self {
478 self.padding = padding;
479 self
480 }
481 pub fn orientation(
488 mut self,
489 orientation: Orientation,
490 ) -> Self {
491 self.orientation = orientation;
492 self
493 }
494 pub fn vertical_alignment(
501 mut self,
502 alignment: VerticalAlignment,
503 ) -> Self {
504 self.vertical_alignment = alignment;
505 self
506 }
507 pub fn horizontal_alignment(
509 mut self,
510 alignment: Alignment,
511 ) -> Self {
512 self.horizontal_alignment = alignment;
513 self
514 }
515}
516#[cfg(feature = "all")]
517impl Widget for Rule {
518 fn render(self, area_old: Rect, buf: &mut Buffer) {
519 self.render_ref(area_old, buf);
520 }
521}
522#[cfg(test)]
523mod tests {
524 use ratatui::widgets::Block;
525 #[test]
526 pub fn test_hr() {
527 use super::presets::test_sets::HORIZONTAL;
528 use super::*;
529 let mut buffer =
530 Buffer::empty(Rect::new(0, 0, 49, 19));
531 Block::bordered()
532 .title_top(
533 Line::raw("Horizontal Rule").centered(),
534 )
535 .title_bottom(
536 Line::raw(" Vertical Alignment: Center ")
537 .centered(),
538 )
539 .render(buffer.area, &mut buffer);
540 Rule::from_set(HORIZONTAL)
541 .horizontal_padding(1)
542 .vertical_alignment(VerticalAlignment::Center)
543 .horizontal()
544 .render(buffer.area, &mut buffer);
545 #[rustfmt::skip]
546 let expected = Buffer::with_lines([
547 "┌────────────────Horizontal Rule────────────────┐",
548 "│ │",
549 "│ │",
550 "│ │",
551 "│ │",
552 "│ │",
553 "│ │",
554 "│ │",
555 "│ │",
556 "│ +─────────────────────+─────────────────────+ │",
557 "│ │",
558 "│ │",
559 "│ │",
560 "│ │",
561 "│ │",
562 "│ │",
563 "│ │",
564 "│ │",
565 "└───────── Vertical Alignment: Center ──────────┘",
566 ]);
567 assert_eq!(buffer, expected);
568 buffer = Buffer::empty(Rect::new(0, 0, 49, 19));
569 Block::bordered()
570 .title_top(
571 Line::raw("Horizontal Rule").centered(),
572 )
573 .title_bottom(
574 Line::raw(" Vertical Alignment: Top ")
575 .centered(),
576 )
577 .render(buffer.area, &mut buffer);
578 Rule::from_set(HORIZONTAL)
579 .horizontal_padding(1)
580 .vertical_alignment(VerticalAlignment::Top)
581 .horizontal()
582 .render(buffer.area, &mut buffer);
583 #[rustfmt::skip]
584 let expected = Buffer::with_lines([
585 "┌────────────────Horizontal Rule────────────────┐",
586 "│ +─────────────────────+─────────────────────+ │",
587 "│ │",
588 "│ │",
589 "│ │",
590 "│ │",
591 "│ │",
592 "│ │",
593 "│ │",
594 "│ │",
595 "│ │",
596 "│ │",
597 "│ │",
598 "│ │",
599 "│ │",
600 "│ │",
601 "│ │",
602 "│ │",
603 "└─────────── Vertical Alignment: Top ───────────┘",
604 ]);
605 assert_eq!(buffer, expected);
606 buffer = Buffer::empty(Rect::new(0, 0, 49, 19));
607 Block::bordered()
608 .title_top(
609 Line::raw("Horizontal Rule").centered(),
610 )
611 .title_bottom(
612 Line::raw(" Vertical Alignment: Bottom ")
613 .centered(),
614 )
615 .render(buffer.area, &mut buffer);
616 Rule::from_set(HORIZONTAL)
617 .horizontal_padding(1)
618 .vertical_alignment(VerticalAlignment::Bottom)
619 .horizontal()
620 .render(buffer.area, &mut buffer);
621 #[rustfmt::skip]
622 let expected = Buffer::with_lines([
623 "┌────────────────Horizontal Rule────────────────┐",
624 "│ │",
625 "│ │",
626 "│ │",
627 "│ │",
628 "│ │",
629 "│ │",
630 "│ │",
631 "│ │",
632 "│ │",
633 "│ │",
634 "│ │",
635 "│ │",
636 "│ │",
637 "│ │",
638 "│ │",
639 "│ │",
640 "│ +─────────────────────+─────────────────────+ │",
641 "└───────── Vertical Alignment: Bottom ──────────┘",
642 ]);
643 assert_eq!(buffer, expected);
644 }
645 #[test]
646 pub fn test_vr() {
647 use super::presets::test_sets::VERTICAL;
648 use super::*;
649 let mut buffer =
650 Buffer::empty(Rect::new(0, 0, 49, 19));
651 Block::bordered()
652 .title_top(
653 Line::raw("Vertical Rule").centered(),
654 )
655 .title_bottom(
656 Line::raw(" Horizontal Alignment: Center ")
657 .centered(),
658 )
659 .render(buffer.area, &mut buffer);
660 Rule::from_set(VERTICAL)
661 .vertical()
662 .vertical_padding(1)
663 .horizontal_alignment(Alignment::Center)
664 .render(buffer.area, &mut buffer);
665 #[rustfmt::skip]
666 let expected = Buffer::with_lines([
667 "┌─────────────────Vertical Rule─────────────────┐",
668 "│ │",
669 "│ + │",
670 "│ │ │",
671 "│ │ │",
672 "│ │ │",
673 "│ │ │",
674 "│ │ │",
675 "│ │ │",
676 "│ + │",
677 "│ │ │",
678 "│ │ │",
679 "│ │ │",
680 "│ │ │",
681 "│ │ │",
682 "│ │ │",
683 "│ + │",
684 "│ │",
685 "└──────── Horizontal Alignment: Center ─────────┘",
686 ]);
687 assert_eq!(buffer, expected);
688 buffer = Buffer::empty(Rect::new(0, 0, 49, 19));
689 Block::bordered()
690 .title_top(
691 Line::raw("Vertical Rule").centered(),
692 )
693 .title_bottom(
694 Line::raw(" Horizontal Alignment: Left ")
695 .centered(),
696 )
697 .render(buffer.area, &mut buffer);
698 Rule::from_set(VERTICAL)
699 .vertical()
700 .vertical_padding(1)
701 .horizontal_alignment(Alignment::Left)
702 .render(buffer.area, &mut buffer);
703 #[rustfmt::skip]
704 let expected = Buffer::with_lines([
705 "┌─────────────────Vertical Rule─────────────────┐",
706 "│ │",
707 "│+ │",
708 "││ │",
709 "││ │",
710 "││ │",
711 "││ │",
712 "││ │",
713 "││ │",
714 "│+ │",
715 "││ │",
716 "││ │",
717 "││ │",
718 "││ │",
719 "││ │",
720 "││ │",
721 "│+ │",
722 "│ │",
723 "└───────── Horizontal Alignment: Left ──────────┘",
724 ]);
725 assert_eq!(buffer, expected);
726 #[rustfmt::skip]
727 let expected = Buffer::with_lines([
728 "┌─────────────────Vertical Rule─────────────────┐",
729 "│ │",
730 "│ +│",
731 "│ ││",
732 "│ ││",
733 "│ ││",
734 "│ ││",
735 "│ ││",
736 "│ ││",
737 "│ +│",
738 "│ ││",
739 "│ ││",
740 "│ ││",
741 "│ ││",
742 "│ ││",
743 "│ ││",
744 "│ +│",
745 "│ │",
746 "└───────── Horizontal Alignment: Right ─────────┘",
747 ]);
748 buffer = Buffer::empty(Rect::new(0, 0, 49, 19));
749 Block::bordered()
750 .title_top(
751 Line::raw("Vertical Rule").centered(),
752 )
753 .title_bottom(
754 Line::raw(" Horizontal Alignment: Right ")
755 .centered(),
756 )
757 .render(buffer.area, &mut buffer);
758 Rule::new()
759 .with_set(VERTICAL)
760 .vertical()
761 .vertical_padding(1)
762 .main_symbol('│')
763 .horizontal_alignment(Alignment::Right)
764 .render(buffer.area, &mut buffer);
765 assert_eq!(buffer, expected);
766 }
767}
768pub mod macros {
769 #[cfg(feature = "utils")]
770 #[macro_export]
771 macro_rules! gen_main {
772 () => {
773 fn main() -> io::Result<()> {
774 let mut terminal = ratatui::init();
775 let app_result = run(&mut terminal);
776 ratatui::restore();
777 app_result
778 }
779 };
780 }
781 #[cfg(feature = "utils")]
782 #[macro_export]
783 macro_rules! gen_example_code {
784 ($fun:item) => {
785 tui_rule::gen_use!();
786 tui_rule::gen_run!($fun);
787 tui_rule::gen_main!();
788 };
789 }
790 #[cfg(feature = "utils")]
791 #[macro_export]
792 macro_rules! gen_run {
793 ($fun:item) => {
794 $fun
795 };
796 }
797 #[cfg(feature = "utils")]
798 #[macro_export]
799 macro_rules! gen_use {
800 () => {
801 use colorgrad::Gradient;
802 use crossterm::event::{
803 self, Event, KeyCode, KeyEvent,
804 KeyEventKind,
805 };
806 use ratatui::{
807 buffer::Buffer,
808 layout::Rect,
809 prelude::{Alignment, Color, Style},
810 text::Line,
811 widgets::{Block, Widget},
812 DefaultTerminal, Frame,
813 };
814 use std::{io, rc::Rc};
815 use tui_rule::*;
816 };
817 }
818 #[macro_export]
819 macro_rules! create_raw_spans {
820 ($string:expr) => {
821 $string
822 .chars()
823 .map(String::from)
824 .map(ratatui::text::Span::from)
825 .collect::<Vec<ratatui::text::Span>>()
826 };
827 }
828}
829
830impl WidgetRef for Rule {
831 fn render_ref(
832 &self,
833 mut area_old: Rect,
834 buf: &mut Buffer,
835 ) {
836 let (p_l, p_r, p_t, p_b) = (
837 self.padding.left,
838 self.padding.right,
839 self.padding.top,
840 self.padding.bottom,
841 );
842 if self.orientation == Orientation::Horizontal {
843 area_old.y = match self.vertical_alignment {
844 VerticalAlignment::Top => area_old
845 .y
846 .saturating_sub(p_b)
847 .saturating_add(p_t),
848 VerticalAlignment::Center => {
849 (area_old.bottom() / 2)
850 .saturating_sub(1 + p_b)
851 .saturating_add(p_t)
852 }
853 VerticalAlignment::Bottom => area_old
854 .bottom()
855 .saturating_sub(
856 1 + p_b
857 + self.area_margin.vertical * 2,
858 )
859 .saturating_add(p_t),
860 }
861 .saturating_sub(self.extra_rep_1 as u16);
862 };
863 if self.orientation == Orientation::Vertical {
864 area_old.x = match self.horizontal_alignment {
865 Alignment::Left => area_old
866 .x
867 .saturating_sub(p_r)
868 .saturating_add(p_l),
869 Alignment::Center => (area_old.right() / 2)
870 .saturating_sub(1 + p_r)
871 .saturating_add(p_l),
872
873 Alignment::Right => {
874 area_old.right().saturating_sub(
875 1 + p_r
876 + self.area_margin.horizontal
877 * 2,
878 )
879 }
880 }
881 .saturating_sub(self.extra_rep_1 as u16);
882 };
883
884 let area = area_old.inner(self.area_margin);
885
886 let ln = create_segment!(
887 self.symbol_set,
888 match self.orientation {
889 Orientation::Vertical => p_t,
890 Orientation::Horizontal => p_l,
891 } as usize,
892 match self.orientation {
893 Orientation::Vertical => p_b,
894 Orientation::Horizontal => p_r,
895 } as usize,
896 match self.orientation {
897 Orientation::Horizontal =>
898 area.width as f32,
899 Orientation::Vertical => area.height as f32,
900 },
901 self.orientation,
902 self.horizontal_alignment,
903 self.vertical_alignment,
904 self.extra_rep_1,
905 self.extra_rep_2
906 );
907
908 let ln = if let Some(boxed) = &self.gradient {
909 match self.bg {
910 Bg::None => Line::from(
911 generate_gradient_text!(ln, boxed),
912 ),
913 _ => Line::from(generate_gradient_text!(
914 ln, boxed, &self.bg
915 )),
916 }
917 } else {
918 Line::from(crate::create_raw_spans!(ln))
919 };
920 match self.orientation {
921 Orientation::Horizontal => {
922 buf.set_line(
923 area.x,
924 area.y,
925 &ln,
926 ln.spans.len() as u16 + 1,
927 );
928 }
929 Orientation::Vertical => {
930 for (y_n, s) in ln.iter().enumerate() {
931 buf.set_span(
932 area.x,
933 area.y + y_n as u16,
934 s,
935 1,
936 );
937 }
938 }
939 }
940 }
941}