1use crate::styles::Style;
8use crate::theme::tokens::Color;
9use crate::theme::use_style;
10use dioxus::prelude::*;
11
12#[derive(Default, Clone, PartialEq, Debug)]
14pub enum BoxDisplay {
15 #[default]
17 Block,
18 Flex,
20 InlineFlex,
22 InlineBlock,
24 Grid,
26 Inline,
28 None,
30}
31
32impl BoxDisplay {
33 #[allow(dead_code)]
34 fn as_str(&self) -> &'static str {
35 match self {
36 BoxDisplay::Block => "block",
37 BoxDisplay::Flex => "flex",
38 BoxDisplay::InlineFlex => "inline-flex",
39 BoxDisplay::InlineBlock => "inline-block",
40 BoxDisplay::Grid => "grid",
41 BoxDisplay::Inline => "inline",
42 BoxDisplay::None => "none",
43 }
44 }
45}
46
47#[derive(Default, Clone, PartialEq, Debug)]
49pub enum FlexDirection {
50 #[default]
52 Row,
53 Column,
55 RowReverse,
57 ColumnReverse,
59}
60
61impl FlexDirection {
62 fn as_str(&self) -> &'static str {
63 match self {
64 FlexDirection::Row => "row",
65 FlexDirection::Column => "column",
66 FlexDirection::RowReverse => "row-reverse",
67 FlexDirection::ColumnReverse => "column-reverse",
68 }
69 }
70}
71
72#[derive(Default, Clone, PartialEq, Debug)]
74pub enum FlexWrap {
75 #[default]
77 NoWrap,
78 Wrap,
80 WrapReverse,
82}
83
84impl FlexWrap {
85 fn as_str(&self) -> &'static str {
86 match self {
87 FlexWrap::NoWrap => "nowrap",
88 FlexWrap::Wrap => "wrap",
89 FlexWrap::WrapReverse => "wrap-reverse",
90 }
91 }
92}
93
94#[derive(Default, Clone, PartialEq, Debug)]
96pub enum JustifyContent {
97 #[default]
99 Start,
100 End,
102 Center,
104 SpaceBetween,
106 SpaceAround,
108 SpaceEvenly,
110}
111
112impl JustifyContent {
113 fn as_str(&self) -> &'static str {
114 match self {
115 JustifyContent::Start => "flex-start",
116 JustifyContent::End => "flex-end",
117 JustifyContent::Center => "center",
118 JustifyContent::SpaceBetween => "space-between",
119 JustifyContent::SpaceAround => "space-around",
120 JustifyContent::SpaceEvenly => "space-evenly",
121 }
122 }
123}
124
125#[derive(Default, Clone, PartialEq, Debug)]
127pub enum AlignItems {
128 #[default]
130 Stretch,
131 Start,
133 End,
135 Center,
137 Baseline,
139}
140
141impl AlignItems {
142 fn as_str(&self) -> &'static str {
143 match self {
144 AlignItems::Stretch => "stretch",
145 AlignItems::Start => "flex-start",
146 AlignItems::End => "flex-end",
147 AlignItems::Center => "center",
148 AlignItems::Baseline => "baseline",
149 }
150 }
151}
152
153#[derive(Default, Clone, PartialEq, Debug)]
155pub enum SpacingSize {
156 None,
158 Xs,
160 #[default]
162 Sm,
163 Md,
165 Lg,
167 Xl,
169 Xxl,
171}
172
173impl SpacingSize {
174 fn as_str(&self) -> &'static str {
175 match self {
176 SpacingSize::None => "none",
177 SpacingSize::Xs => "xs",
178 SpacingSize::Sm => "sm",
179 SpacingSize::Md => "md",
180 SpacingSize::Lg => "lg",
181 SpacingSize::Xl => "xl",
182 SpacingSize::Xxl => "xxl",
183 }
184 }
185}
186
187#[derive(Default, Clone, PartialEq, Debug)]
189pub enum RadiusSize {
190 None,
192 Sm,
194 #[default]
196 Md,
197 Lg,
199 Xl,
201 Full,
203}
204
205impl RadiusSize {
206 fn as_str(&self) -> &'static str {
207 match self {
208 RadiusSize::None => "none",
209 RadiusSize::Sm => "sm",
210 RadiusSize::Md => "md",
211 RadiusSize::Lg => "lg",
212 RadiusSize::Xl => "xl",
213 RadiusSize::Full => "full",
214 }
215 }
216}
217
218#[derive(Default, Clone, PartialEq, Debug)]
220pub enum ShadowSize {
221 #[default]
223 None,
224 Sm,
226 Md,
228 Lg,
230 Xl,
232 Inner,
234}
235
236impl ShadowSize {
237 fn as_str(&self) -> &'static str {
238 match self {
239 ShadowSize::None => "none",
240 ShadowSize::Sm => "sm",
241 ShadowSize::Md => "md",
242 ShadowSize::Lg => "lg",
243 ShadowSize::Xl => "xl",
244 ShadowSize::Inner => "inner",
245 }
246 }
247}
248
249#[derive(Clone, PartialEq, Debug)]
251pub enum BackgroundColor {
252 Primary,
254 Secondary,
256 Background,
258 Foreground,
260 Muted,
262 Accent,
264 Card,
266 Popover,
268 Destructive,
270 Success,
272 Warning,
274 Transparent,
276 Custom(Color),
278}
279
280impl Default for BackgroundColor {
281 fn default() -> Self {
282 BackgroundColor::Transparent
283 }
284}
285
286#[derive(Default, Clone, PartialEq, Debug)]
288pub enum BorderWidth {
289 #[default]
291 None,
292 Thin,
294 Medium,
296 Thick,
298}
299
300impl BorderWidth {
301 fn as_px(&self) -> u8 {
302 match self {
303 BorderWidth::None => 0,
304 BorderWidth::Thin => 1,
305 BorderWidth::Medium => 2,
306 BorderWidth::Thick => 4,
307 }
308 }
309}
310
311#[derive(Default, Clone, PartialEq, Debug)]
313pub enum Overflow {
314 #[default]
316 Visible,
317 Hidden,
319 Scroll,
321 Auto,
323}
324
325impl Overflow {
326 fn as_str(&self) -> &'static str {
327 match self {
328 Overflow::Visible => "visible",
329 Overflow::Hidden => "hidden",
330 Overflow::Scroll => "scroll",
331 Overflow::Auto => "auto",
332 }
333 }
334}
335
336#[derive(Default, Clone, PartialEq, Debug)]
338pub enum Position {
339 #[default]
341 Static,
342 Relative,
344 Absolute,
346 Fixed,
348 Sticky,
350}
351
352impl Position {
353 fn as_str(&self) -> &'static str {
354 match self {
355 Position::Static => "static",
356 Position::Relative => "relative",
357 Position::Absolute => "absolute",
358 Position::Fixed => "fixed",
359 Position::Sticky => "sticky",
360 }
361 }
362}
363
364#[derive(Props, Clone, PartialEq)]
366pub struct BoxProps {
367 pub children: Element,
369 #[props(default)]
371 pub display: BoxDisplay,
372 #[props(default)]
374 pub flex_direction: FlexDirection,
375 #[props(default)]
377 pub flex_wrap: FlexWrap,
378 #[props(default)]
380 pub justify_content: JustifyContent,
381 #[props(default)]
383 pub align_items: AlignItems,
384 #[props(default)]
386 pub padding: SpacingSize,
387 #[props(default)]
389 pub px: Option<SpacingSize>,
390 #[props(default)]
392 pub py: Option<SpacingSize>,
393 #[props(default)]
395 pub pt: Option<SpacingSize>,
396 #[props(default)]
398 pub pr: Option<SpacingSize>,
399 #[props(default)]
401 pub pb: Option<SpacingSize>,
402 #[props(default)]
404 pub pl: Option<SpacingSize>,
405 #[props(default)]
407 pub margin: SpacingSize,
408 #[props(default)]
410 pub mx: Option<SpacingSize>,
411 #[props(default)]
413 pub my: Option<SpacingSize>,
414 #[props(default)]
416 pub mt: Option<SpacingSize>,
417 #[props(default)]
419 pub mr: Option<SpacingSize>,
420 #[props(default)]
422 pub mb: Option<SpacingSize>,
423 #[props(default)]
425 pub ml: Option<SpacingSize>,
426 #[props(default)]
428 pub gap: SpacingSize,
429 #[props(default)]
431 pub background: BackgroundColor,
432 #[props(default)]
434 pub border_radius: RadiusSize,
435 #[props(default)]
437 pub border: BorderWidth,
438 #[props(default)]
440 pub border_color: Option<BackgroundColor>,
441 #[props(default)]
443 pub shadow: ShadowSize,
444 #[props(default)]
446 pub width: Option<String>,
447 #[props(default)]
449 pub height: Option<String>,
450 #[props(default)]
452 pub min_width: Option<String>,
453 #[props(default)]
455 pub min_height: Option<String>,
456 #[props(default)]
458 pub max_width: Option<String>,
459 #[props(default)]
461 pub max_height: Option<String>,
462 #[props(default)]
464 pub overflow: Overflow,
465 #[props(default)]
467 pub position: Position,
468 #[props(default)]
470 pub top: Option<String>,
471 #[props(default)]
473 pub right: Option<String>,
474 #[props(default)]
476 pub bottom: Option<String>,
477 #[props(default)]
479 pub left: Option<String>,
480 #[props(default)]
482 pub z_index: Option<i16>,
483 #[props(default)]
485 pub opacity: Option<f32>,
486 #[props(default)]
488 pub cursor: Option<String>,
489 #[props(default)]
491 pub onclick: Option<EventHandler<MouseEvent>>,
492 #[props(default)]
494 pub onmouseenter: Option<EventHandler<MouseEvent>>,
495 #[props(default)]
497 pub onmouseleave: Option<EventHandler<MouseEvent>>,
498 #[props(default)]
500 pub style: Option<String>,
501 #[props(default)]
503 pub class: Option<String>,
504 #[props(default)]
506 pub id: Option<String>,
507}
508
509#[component]
529pub fn Box(props: BoxProps) -> Element {
530 let display = props.display.clone();
531 let flex_direction = props.flex_direction.clone();
532 let flex_wrap = props.flex_wrap.clone();
533 let justify_content = props.justify_content.clone();
534 let align_items = props.align_items.clone();
535 let padding = props.padding.clone();
536 let px = props.px.clone();
537 let py = props.py.clone();
538 let pt = props.pt.clone();
539 let pr = props.pr.clone();
540 let pb = props.pb.clone();
541 let pl = props.pl.clone();
542 let margin = props.margin.clone();
543 let mx = props.mx.clone();
544 let my = props.my.clone();
545 let mt = props.mt.clone();
546 let mr = props.mr.clone();
547 let mb = props.mb.clone();
548 let ml = props.ml.clone();
549 let gap = props.gap.clone();
550 let background = props.background.clone();
551 let border_radius = props.border_radius.clone();
552 let border = props.border.clone();
553 let border_color = props.border_color.clone();
554 let shadow = props.shadow.clone();
555 let overflow = props.overflow.clone();
556 let position = props.position.clone();
557
558 let style = use_style(move |t| {
559 let mut style = Style::new();
560
561 style = match display {
563 BoxDisplay::Block => style.block(),
564 BoxDisplay::Flex => style.flex(),
565 BoxDisplay::InlineFlex => style.inline_flex(),
566 BoxDisplay::InlineBlock => style.inline_block(),
567 BoxDisplay::Grid => style.grid(),
568 BoxDisplay::Inline => style,
569 BoxDisplay::None => style.hidden(),
570 };
571
572 if display == BoxDisplay::Flex || display == BoxDisplay::InlineFlex {
574 style = Style {
575 flex_direction: Some(flex_direction.as_str().into()),
576 ..style
577 };
578 style = Style {
579 flex_wrap: Some(flex_wrap.as_str().into()),
580 ..style
581 };
582 style = Style {
583 justify_content: Some(justify_content.as_str().into()),
584 ..style
585 };
586 style = Style {
587 align_items: Some(align_items.as_str().into()),
588 ..style
589 };
590 }
591
592 if gap != SpacingSize::None
594 && (display == BoxDisplay::Flex
595 || display == BoxDisplay::InlineFlex
596 || display == BoxDisplay::Grid)
597 {
598 style = style.gap(&t.spacing, gap.as_str());
599 }
600
601 if padding != SpacingSize::None {
603 style = style.p(&t.spacing, padding.as_str());
604 }
605 if let Some(px_size) = &px {
606 if *px_size != SpacingSize::None {
607 style = style.px(&t.spacing, px_size.as_str());
608 }
609 }
610 if let Some(py_size) = &py {
611 if *py_size != SpacingSize::None {
612 style = style.py(&t.spacing, py_size.as_str());
613 }
614 }
615 if let Some(pt_size) = &pt {
616 if *pt_size != SpacingSize::None {
617 style = style.pt(&t.spacing, pt_size.as_str());
618 }
619 }
620 if let Some(pr_size) = &pr {
621 if *pr_size != SpacingSize::None {
622 style = style.pr(&t.spacing, pr_size.as_str());
623 }
624 }
625 if let Some(pb_size) = &pb {
626 if *pb_size != SpacingSize::None {
627 style = style.pb(&t.spacing, pb_size.as_str());
628 }
629 }
630 if let Some(pl_size) = &pl {
631 if *pl_size != SpacingSize::None {
632 style = style.pl(&t.spacing, pl_size.as_str());
633 }
634 }
635
636 if margin != SpacingSize::None {
638 style = style.m(&t.spacing, margin.as_str());
639 }
640 if let Some(mx_size) = &mx {
641 if *mx_size != SpacingSize::None {
642 style = style.mx(&t.spacing, mx_size.as_str());
643 }
644 }
645 if let Some(my_size) = &my {
646 if *my_size != SpacingSize::None {
647 style = style.my(&t.spacing, my_size.as_str());
648 }
649 }
650 if let Some(mt_size) = &mt {
651 if *mt_size != SpacingSize::None {
652 style = style.mt(&t.spacing, mt_size.as_str());
653 }
654 }
655 if let Some(mr_size) = &mr {
656 if *mr_size != SpacingSize::None {
657 style = style.mr(&t.spacing, mr_size.as_str());
658 }
659 }
660 if let Some(mb_size) = &mb {
661 if *mb_size != SpacingSize::None {
662 style = style.mb(&t.spacing, mb_size.as_str());
663 }
664 }
665 if let Some(ml_size) = &ml {
666 if *ml_size != SpacingSize::None {
667 style = style.ml(&t.spacing, ml_size.as_str());
668 }
669 }
670
671 let bg_color = match &background {
673 BackgroundColor::Primary => t.colors.primary.clone(),
674 BackgroundColor::Secondary => t.colors.secondary.clone(),
675 BackgroundColor::Background => t.colors.background.clone(),
676 BackgroundColor::Foreground => t.colors.foreground.clone(),
677 BackgroundColor::Muted => t.colors.muted.clone(),
678 BackgroundColor::Accent => t.colors.accent.clone(),
679 BackgroundColor::Card => t.colors.card.clone(),
680 BackgroundColor::Popover => t.colors.popover.clone(),
681 BackgroundColor::Destructive => t.colors.destructive.clone(),
682 BackgroundColor::Success => t.colors.success.clone(),
683 BackgroundColor::Warning => t.colors.warning.clone(),
684 BackgroundColor::Transparent => Color::new_rgba(0, 0, 0, 0.0),
685 BackgroundColor::Custom(c) => c.clone(),
686 };
687 style = style.bg(&bg_color);
688
689 style = style.rounded(&t.radius, border_radius.as_str());
691
692 if border != BorderWidth::None {
694 let border_c = match &border_color {
695 Some(BackgroundColor::Primary) => t.colors.primary.clone(),
696 Some(BackgroundColor::Secondary) => t.colors.secondary.clone(),
697 Some(BackgroundColor::Background) => t.colors.background.clone(),
698 Some(BackgroundColor::Foreground) => t.colors.foreground.clone(),
699 Some(BackgroundColor::Muted) => t.colors.muted.clone(),
700 Some(BackgroundColor::Accent) => t.colors.accent.clone(),
701 Some(BackgroundColor::Card) => t.colors.card.clone(),
702 Some(BackgroundColor::Popover) => t.colors.popover.clone(),
703 Some(BackgroundColor::Destructive) => t.colors.destructive.clone(),
704 Some(BackgroundColor::Success) => t.colors.success.clone(),
705 Some(BackgroundColor::Warning) => t.colors.warning.clone(),
706 Some(BackgroundColor::Transparent) => Color::new_rgba(0, 0, 0, 0.0),
707 Some(BackgroundColor::Custom(c)) => c.clone(),
708 None => t.colors.border.clone(),
709 };
710 style = style.border(border.as_px(), &border_c);
711 }
712
713 if shadow != ShadowSize::None {
715 style = style.shadow_themed(&t, shadow.as_str());
716 }
717
718 style = Style {
720 overflow: Some(overflow.as_str().into()),
721 ..style
722 };
723
724 style = Style {
726 position: Some(position.as_str().into()),
727 ..style
728 };
729
730 if let Some(op) = props.opacity {
732 style = style.opacity(op.clamp(0.0, 1.0));
733 }
734
735 if let Some(c) = &props.cursor {
737 style = style.cursor(c);
738 }
739
740 style.build()
741 });
742
743 let mut additional_styles = String::new();
745
746 if let Some(w) = &props.width {
748 additional_styles.push_str(&format!("width: {}; ", w));
749 }
750 if let Some(h) = &props.height {
752 additional_styles.push_str(&format!("height: {}; ", h));
753 }
754 if let Some(mw) = &props.min_width {
756 additional_styles.push_str(&format!("min-width: {}; ", mw));
757 }
758 if let Some(mh) = &props.min_height {
760 additional_styles.push_str(&format!("min-height: {}; ", mh));
761 }
762 if let Some(mw) = &props.max_width {
764 additional_styles.push_str(&format!("max-width: {}; ", mw));
765 }
766 if let Some(mh) = &props.max_height {
768 additional_styles.push_str(&format!("max-height: {}; ", mh));
769 }
770 if let Some(top) = &props.top {
772 additional_styles.push_str(&format!("top: {}; ", top));
773 }
774 if let Some(right) = &props.right {
775 additional_styles.push_str(&format!("right: {}; ", right));
776 }
777 if let Some(bottom) = &props.bottom {
778 additional_styles.push_str(&format!("bottom: {}; ", bottom));
779 }
780 if let Some(left) = &props.left {
781 additional_styles.push_str(&format!("left: {}; ", left));
782 }
783 if let Some(z) = props.z_index {
785 additional_styles.push_str(&format!("z-index: {}; ", z));
786 }
787
788 let final_style = if let Some(custom) = &props.style {
790 format!("{} {}{}", style(), additional_styles, custom)
791 } else {
792 format!("{} {}", style(), additional_styles)
793 };
794
795 let class = props.class.clone().unwrap_or_default();
796 let id = props.id.clone().unwrap_or_default();
797
798 rsx! {
799 div {
800 style: "{final_style}",
801 class: "{class}",
802 id: "{id}",
803 onclick: move |e| {
804 if let Some(handler) = &props.onclick {
805 handler.call(e);
806 }
807 },
808 onmouseenter: move |e| {
809 if let Some(handler) = &props.onmouseenter {
810 handler.call(e);
811 }
812 },
813 onmouseleave: move |e| {
814 if let Some(handler) = &props.onmouseleave {
815 handler.call(e);
816 }
817 },
818 {props.children}
819 }
820 }
821}
822
823#[component]
827pub fn VStack(
828 children: Element,
829 #[props(default)] gap: SpacingSize,
830 #[props(default)] padding: SpacingSize,
831 #[props(default)] align: AlignItems,
832 #[props(default)] justify: JustifyContent,
833 #[props(default)] background: BackgroundColor,
834 #[props(default)] width: Option<String>,
835 #[props(default)] height: Option<String>,
836 #[props(default)] style: Option<String>,
837 #[props(default)] class: Option<String>,
838) -> Element {
839 rsx! {
840 Box {
841 display: BoxDisplay::Flex,
842 flex_direction: FlexDirection::Column,
843 align_items: align,
844 justify_content: justify,
845 gap: gap,
846 padding: padding,
847 background: background,
848 width: width,
849 height: height,
850 style: style,
851 class: class,
852 {children}
853 }
854 }
855}
856
857#[component]
861pub fn HStack(
862 children: Element,
863 #[props(default)] gap: SpacingSize,
864 #[props(default)] padding: SpacingSize,
865 #[props(default)] align: AlignItems,
866 #[props(default)] justify: JustifyContent,
867 #[props(default)] background: BackgroundColor,
868 #[props(default)] width: Option<String>,
869 #[props(default)] height: Option<String>,
870 #[props(default)] style: Option<String>,
871 #[props(default)] class: Option<String>,
872) -> Element {
873 rsx! {
874 Box {
875 display: BoxDisplay::Flex,
876 flex_direction: FlexDirection::Row,
877 align_items: align,
878 justify_content: justify,
879 gap: gap,
880 padding: padding,
881 background: background,
882 width: width,
883 height: height,
884 style: style,
885 class: class,
886 {children}
887 }
888 }
889}
890
891#[component]
895pub fn Center(
896 children: Element,
897 #[props(default)] padding: SpacingSize,
898 #[props(default)] background: BackgroundColor,
899 #[props(default)] width: Option<String>,
900 #[props(default)] height: Option<String>,
901 #[props(default)] style: Option<String>,
902 #[props(default)] class: Option<String>,
903) -> Element {
904 rsx! {
905 Box {
906 display: BoxDisplay::Flex,
907 align_items: AlignItems::Center,
908 justify_content: JustifyContent::Center,
909 padding: padding,
910 background: background,
911 width: width,
912 height: height,
913 style: style,
914 class: class,
915 {children}
916 }
917 }
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923
924 #[test]
925 fn test_box_display_variants() {
926 assert_eq!(BoxDisplay::Flex.as_str(), "flex");
927 assert_eq!(BoxDisplay::Block.as_str(), "block");
928 assert_eq!(BoxDisplay::None.as_str(), "none");
929 }
930
931 #[test]
932 fn test_flex_direction_variants() {
933 assert_eq!(FlexDirection::Row.as_str(), "row");
934 assert_eq!(FlexDirection::Column.as_str(), "column");
935 }
936
937 #[test]
938 fn test_justify_content_variants() {
939 assert_eq!(JustifyContent::Center.as_str(), "center");
940 assert_eq!(JustifyContent::SpaceBetween.as_str(), "space-between");
941 }
942
943 #[test]
944 fn test_align_items_variants() {
945 assert_eq!(AlignItems::Center.as_str(), "center");
946 assert_eq!(AlignItems::Stretch.as_str(), "stretch");
947 }
948
949 #[test]
950 fn test_spacing_size_to_str() {
951 assert_eq!(SpacingSize::Md.as_str(), "md");
952 assert_eq!(SpacingSize::Lg.as_str(), "lg");
953 }
954
955 #[test]
956 fn test_border_width_to_px() {
957 assert_eq!(BorderWidth::None.as_px(), 0);
958 assert_eq!(BorderWidth::Thin.as_px(), 1);
959 assert_eq!(BorderWidth::Medium.as_px(), 2);
960 assert_eq!(BorderWidth::Thick.as_px(), 4);
961 }
962}