1use std::fmt;
21
22use bevy::text::{FontWeight, Justify, LineBreak};
23use bevy::ui::{
24 AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, FocusPolicy,
25 GridAutoFlow, GridPlacement, GridTrack, JustifyContent, JustifyItems, JustifySelf,
26 OverflowAxis, PositionType, RepeatedGridTrack,
27};
28use serde::de::{self, Deserializer, MapAccess, Visitor};
29use serde::{Deserialize, Serialize};
30
31pub type NodeId = u32;
34
35pub const ROOT_ID: NodeId = 0;
36
37#[derive(Debug, Clone, Deserialize)]
41#[serde(tag = "op", rename_all = "camelCase")]
42pub enum Op {
43 Reset,
46 Create {
48 id: NodeId,
49 kind: String,
50 #[serde(default)]
51 props: Props,
52 #[serde(default)]
55 text: Option<String>,
56 },
57 CreateText { id: NodeId, text: String },
59 CreateTextSpan { id: NodeId, text: String },
62 Append { parent: NodeId, child: NodeId },
64 Insert {
66 parent: NodeId,
67 child: NodeId,
68 before: NodeId,
69 },
70 Remove { parent: NodeId, child: NodeId },
72 Update {
88 id: NodeId,
89 #[serde(default)]
90 props: Props,
91 #[serde(default)]
93 unset: Vec<String>,
94 #[serde(default, rename = "styleUnset")]
98 style_unset: Vec<String>,
99 },
100 UpdateText { id: NodeId, text: String },
102 Draw { id: NodeId, cmds: Vec<DrawCmd> },
110}
111
112#[derive(Debug, Clone, Default, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct Props {
119 #[serde(default)]
121 pub style: Option<Style>,
122 #[serde(default)]
125 pub hover_style: Option<Style>,
126 #[serde(default)]
128 pub press_style: Option<Style>,
129 #[serde(default)]
133 pub focus_style: Option<Style>,
134 #[serde(default)]
136 pub on_click: bool,
137 #[serde(default)]
139 pub on_pointer_down: bool,
140 #[serde(default)]
143 pub on_pointer_move: bool,
144 #[serde(default)]
146 pub on_pointer_up: bool,
147 #[serde(default)]
150 pub on_pointer_enter: bool,
151 #[serde(default)]
154 pub on_pointer_leave: bool,
155
156 #[serde(default)]
162 pub scroll_top: Option<f32>,
163 #[serde(default)]
165 pub scroll_left: Option<f32>,
166 #[serde(default)]
170 pub scroll_step: Option<f32>,
171 #[serde(default)]
176 pub on_scroll: bool,
177 #[serde(default)]
182 pub on_wheel: bool,
183
184 #[serde(default)]
189 pub animated: Option<crate::animations::AnimatedBindings>,
190 #[serde(default)]
194 pub anchor: Option<crate::anchor::Anchor>,
195
196 #[serde(default)]
200 pub src: Option<String>,
201 #[serde(default)]
203 pub tint: Option<String>,
204 #[serde(default)]
206 pub flip_x: bool,
207 #[serde(default)]
209 pub flip_y: bool,
210 #[serde(default)]
213 pub image_mode: Option<ImageMode>,
214 #[serde(default)]
218 pub source_rect: Option<SourceRect>,
219 #[serde(default)]
222 pub atlas: Option<AtlasSpec>,
223 #[serde(default)]
226 pub visual_box: Option<String>,
227
228 #[serde(default)]
236 pub draw: Option<Vec<DrawCmd>>,
237 #[serde(default)]
242 pub on_resize: bool,
243
244 #[serde(default)]
250 pub target: Option<String>,
251
252 #[serde(default)]
257 pub value: Option<String>,
258 #[serde(default)]
260 pub max_length: Option<usize>,
261 #[serde(default)]
263 pub multiline: bool,
264 #[serde(default)]
266 pub on_change: bool,
267 #[serde(default)]
269 pub autofocus: bool,
270 #[serde(default)]
274 pub selection_start: Option<usize>,
275 #[serde(default)]
277 pub selection_end: Option<usize>,
278 #[serde(default)]
280 pub aria_label: Option<String>,
281 #[serde(default)]
283 pub on_select: bool,
284 #[serde(default)]
286 pub on_focus: bool,
287 #[serde(default)]
289 pub on_blur: bool,
290}
291
292pub use crate::canvas::DrawCmd;
296
297#[derive(Debug, Clone, Default, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct Style {
311 #[serde(default, deserialize_with = "de_display")]
313 pub display: Option<Display>,
314 #[serde(default, deserialize_with = "de_box_sizing")]
315 pub box_sizing: Option<BoxSizing>,
316 #[serde(default, deserialize_with = "de_position_type")]
317 pub position_type: Option<PositionType>,
318 #[serde(default, deserialize_with = "de_overflow_axis")]
319 pub overflow_x: Option<OverflowAxis>,
320 #[serde(default, deserialize_with = "de_overflow_axis")]
321 pub overflow_y: Option<OverflowAxis>,
322 #[serde(default)]
323 pub scrollbar_width: Option<f32>,
324
325 #[serde(default)]
327 pub left: Option<Length>,
328 #[serde(default)]
329 pub right: Option<Length>,
330 #[serde(default)]
331 pub top: Option<Length>,
332 #[serde(default)]
333 pub bottom: Option<Length>,
334
335 #[serde(default)]
337 pub width: Option<Length>,
338 #[serde(default)]
339 pub height: Option<Length>,
340 #[serde(default)]
341 pub min_width: Option<Length>,
342 #[serde(default)]
343 pub min_height: Option<Length>,
344 #[serde(default)]
345 pub max_width: Option<Length>,
346 #[serde(default)]
347 pub max_height: Option<Length>,
348 #[serde(default)]
349 pub aspect_ratio: Option<f32>,
350
351 #[serde(default, deserialize_with = "de_align_items")]
353 pub align_items: Option<AlignItems>,
354 #[serde(default, deserialize_with = "de_justify_items")]
355 pub justify_items: Option<JustifyItems>,
356 #[serde(default, deserialize_with = "de_align_self")]
357 pub align_self: Option<AlignSelf>,
358 #[serde(default, deserialize_with = "de_justify_self")]
359 pub justify_self: Option<JustifySelf>,
360 #[serde(default, deserialize_with = "de_align_content")]
361 pub align_content: Option<AlignContent>,
362 #[serde(default, deserialize_with = "de_justify_content")]
363 pub justify_content: Option<JustifyContent>,
364
365 #[serde(default)]
367 pub margin: Option<Rect>,
368 #[serde(default)]
369 pub padding: Option<Rect>,
370 #[serde(default)]
371 pub border: Option<Rect>,
372
373 #[serde(default, deserialize_with = "de_flex_direction")]
375 pub flex_direction: Option<FlexDirection>,
376 #[serde(default, deserialize_with = "de_flex_wrap")]
377 pub flex_wrap: Option<FlexWrap>,
378 #[serde(default)]
379 pub flex_grow: Option<f32>,
380 #[serde(default)]
381 pub flex_shrink: Option<f32>,
382 #[serde(default)]
383 pub flex_basis: Option<Length>,
384 #[serde(default)]
385 pub gap: Option<Length>,
386 #[serde(default)]
387 pub row_gap: Option<Length>,
388 #[serde(default)]
389 pub column_gap: Option<Length>,
390
391 #[serde(default, deserialize_with = "de_grid_auto_flow")]
393 pub grid_auto_flow: Option<GridAutoFlow>,
394 #[serde(default, deserialize_with = "de_grid_template")]
396 pub grid_template_rows: Option<Vec<RepeatedGridTrack>>,
397 #[serde(default, deserialize_with = "de_grid_template")]
398 pub grid_template_columns: Option<Vec<RepeatedGridTrack>>,
399 #[serde(default, deserialize_with = "de_grid_auto_tracks")]
401 pub grid_auto_rows: Option<Vec<GridTrack>>,
402 #[serde(default, deserialize_with = "de_grid_auto_tracks")]
403 pub grid_auto_columns: Option<Vec<GridTrack>>,
404 #[serde(default, deserialize_with = "de_grid_placement")]
406 pub grid_row: Option<GridPlacement>,
407 #[serde(default, deserialize_with = "de_grid_placement")]
408 pub grid_column: Option<GridPlacement>,
409
410 #[serde(default)]
413 pub background_color: Option<String>,
414 #[serde(default)]
417 pub border_color: Option<BorderColorSpec>,
418 #[serde(default)]
421 pub border_radius: Option<Rect>,
422 #[serde(default)]
423 pub outline: Option<OutlineSpec>,
424 #[serde(default)]
425 pub box_shadow: Option<BoxShadowList>,
426 #[serde(default)]
435 pub filter: Option<FilterSpec>,
436 #[serde(default)]
440 pub background_gradient: Option<GradientList>,
441 #[serde(default)]
444 pub border_gradient: Option<GradientList>,
445 #[serde(default)]
446 pub z_index: Option<i32>,
447 #[serde(default)]
451 pub global_z_index: Option<i32>,
452 #[serde(default, deserialize_with = "de_focus_policy")]
458 pub focus_policy: Option<FocusPolicy>,
459 #[serde(default)]
467 pub cursor: Option<String>,
468
469 #[serde(default)]
474 pub transform: Option<Transform>,
475 #[serde(default)]
478 pub opacity: Option<f32>,
479 #[serde(default)]
484 pub transition: Option<crate::transition::Transition>,
485
486 #[serde(default)]
489 pub color: Option<String>,
490 #[serde(default)]
493 pub font_size: Option<FontSize>,
494 #[serde(default, deserialize_with = "de_font_weight")]
497 pub font_weight: Option<FontWeight>,
498 #[serde(default)]
502 pub font_family: Option<String>,
503 #[serde(default, deserialize_with = "de_text_align")]
506 pub text_align: Option<Justify>,
507 #[serde(default)]
510 pub line_height: Option<LineHeightSpec>,
511 #[serde(default)]
514 pub letter_spacing: Option<LetterSpacingSpec>,
515 #[serde(default)]
517 pub text_shadow: Option<TextShadowSpec>,
518 #[serde(default, deserialize_with = "de_line_break")]
522 pub line_break: Option<LineBreak>,
523}
524
525pub mod style_groups {
531 pub const LAYOUT: u32 = 1 << 0;
533 pub const BACKGROUND: u32 = 1 << 1;
535 pub const TRANSFORM: u32 = 1 << 2;
537 pub const BORDER_COLOR: u32 = 1 << 3;
539 pub const OUTLINE: u32 = 1 << 4;
541 pub const BOX_SHADOW: u32 = 1 << 5;
543 pub const BG_GRADIENT: u32 = 1 << 6;
545 pub const BORDER_GRADIENT: u32 = 1 << 7;
547 pub const TEXT_SHADOW: u32 = 1 << 8;
549 pub const Z_INDEX: u32 = 1 << 9;
551 pub const GLOBAL_Z_INDEX: u32 = 1 << 10;
553 pub const FOCUS_POLICY: u32 = 1 << 11;
555 pub const FILTER: u32 = 1 << 12;
557 pub const TRANSITION: u32 = 1 << 13;
561 pub const SCROLL_TRANSITION: u32 = 1 << 14;
563 pub const TEXT: u32 = 1 << 15;
567 pub const TEXT_LAYOUT: u32 = 1 << 16;
569 pub const CURSOR: u32 = 1 << 17;
572}
573
574macro_rules! with_style_fields {
591 ($cb:ident) => {
592 $cb! {
593 (display, "display", (LAYOUT), overlay),
594 (box_sizing, "boxSizing", (LAYOUT), overlay),
595 (position_type, "positionType", (LAYOUT), overlay),
596 (overflow_x, "overflowX", (LAYOUT), overlay),
597 (overflow_y, "overflowY", (LAYOUT), overlay),
598 (scrollbar_width, "scrollbarWidth", (LAYOUT), overlay),
599 (left, "left", (LAYOUT), overlay),
600 (right, "right", (LAYOUT), overlay),
601 (top, "top", (LAYOUT), overlay),
602 (bottom, "bottom", (LAYOUT), overlay),
603 (width, "width", (LAYOUT | TRANSITION), overlay),
604 (height, "height", (LAYOUT | TRANSITION), overlay),
605 (min_width, "minWidth", (LAYOUT), overlay),
606 (min_height, "minHeight", (LAYOUT), overlay),
607 (max_width, "maxWidth", (LAYOUT | TRANSITION), overlay),
608 (max_height, "maxHeight", (LAYOUT | TRANSITION), overlay),
609 (aspect_ratio, "aspectRatio", (LAYOUT), overlay),
610 (align_items, "alignItems", (LAYOUT), overlay),
611 (justify_items, "justifyItems", (LAYOUT), overlay),
612 (align_self, "alignSelf", (LAYOUT), overlay),
613 (justify_self, "justifySelf", (LAYOUT), overlay),
614 (align_content, "alignContent", (LAYOUT), overlay),
615 (justify_content, "justifyContent", (LAYOUT), overlay),
616 (margin, "margin", (LAYOUT), overlay),
617 (padding, "padding", (LAYOUT), overlay),
618 (border, "border", (LAYOUT), overlay),
619 (flex_direction, "flexDirection", (LAYOUT), overlay),
620 (flex_wrap, "flexWrap", (LAYOUT), overlay),
621 (flex_grow, "flexGrow", (LAYOUT), overlay),
622 (flex_shrink, "flexShrink", (LAYOUT), overlay),
623 (flex_basis, "flexBasis", (LAYOUT), overlay),
624 (gap, "gap", (LAYOUT), overlay),
625 (row_gap, "rowGap", (LAYOUT), overlay),
626 (column_gap, "columnGap", (LAYOUT), overlay),
627 (grid_auto_flow, "gridAutoFlow", (LAYOUT), overlay),
628 (grid_template_rows, "gridTemplateRows", (LAYOUT), overlay),
629 (grid_template_columns, "gridTemplateColumns", (LAYOUT), overlay),
630 (grid_auto_rows, "gridAutoRows", (LAYOUT), overlay),
631 (grid_auto_columns, "gridAutoColumns", (LAYOUT), overlay),
632 (grid_row, "gridRow", (LAYOUT), overlay),
633 (grid_column, "gridColumn", (LAYOUT), overlay),
634 (background_color, "backgroundColor", (BACKGROUND | TRANSITION), overlay),
635 (border_color, "borderColor", (BORDER_COLOR), overlay),
636 (border_radius, "borderRadius", (LAYOUT), overlay),
637 (outline, "outline", (OUTLINE), overlay),
638 (box_shadow, "boxShadow", (BOX_SHADOW), overlay),
639 (filter, "filter", (BACKGROUND | FILTER), no_overlay),
640 (background_gradient, "backgroundGradient", (BG_GRADIENT), overlay),
641 (border_gradient, "borderGradient", (BORDER_GRADIENT), overlay),
642 (z_index, "zIndex", (Z_INDEX), overlay),
643 (global_z_index, "globalZIndex", (GLOBAL_Z_INDEX), overlay),
644 (focus_policy, "focusPolicy", (FOCUS_POLICY), no_overlay),
645 (cursor, "cursor", (CURSOR), overlay),
646 (
647 transform,
648 "transform",
649 (TRANSFORM | TRANSITION),
650 overlay
651 ),
652 (
653 opacity,
654 "opacity",
655 (BACKGROUND | BG_GRADIENT | BORDER_GRADIENT | TEXT_SHADOW | TRANSITION | TEXT),
656 overlay
657 ),
658 (
659 transition,
660 "transition",
661 (TRANSITION | SCROLL_TRANSITION),
662 overlay
663 ),
664 (color, "color", (TEXT), overlay),
665 (font_size, "fontSize", (TEXT), overlay),
666 (font_weight, "fontWeight", (TEXT), overlay),
667 (font_family, "fontFamily", (TEXT), overlay),
668 (text_align, "textAlign", (TEXT_LAYOUT), overlay),
669 (line_height, "lineHeight", (TEXT), overlay),
670 (letter_spacing, "letterSpacing", (TEXT), overlay),
671 (text_shadow, "textShadow", (TEXT_SHADOW), overlay),
672 (line_break, "lineBreak", (TEXT_LAYOUT), overlay),
673 }
674 };
675}
676pub(crate) use with_style_fields;
677
678#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
681pub struct StyleDirty(pub u32);
682
683impl StyleDirty {
684 pub const NONE: Self = Self(0);
686 pub const ALL: Self = Self(u32::MAX);
688
689 pub fn intersects(self, groups: u32) -> bool {
691 self.0 & groups != 0
692 }
693
694 pub fn any(self) -> bool {
696 self.0 != 0
697 }
698}
699
700#[derive(Debug, Clone, Copy, Default)]
704pub struct PropsDirty {
705 pub style: StyleDirty,
707 pub hover_style: bool,
709 pub press_style: bool,
711 pub focus_style: bool,
713 pub pointer: bool,
715 pub scroll_listener: bool,
717 pub wheel: bool,
719 pub scroll_step: bool,
721 pub animated: bool,
723 pub anchor: bool,
725 pub image: bool,
728 pub target: bool,
730 pub editable_handlers: bool,
733 pub aria_label: bool,
735}
736
737impl PropsDirty {
738 pub fn any_style_variant(&self) -> bool {
741 self.style.any() || self.hover_style || self.press_style || self.focus_style
742 }
743}
744
745#[derive(Debug, Default)]
750pub struct UpdateEvents {
751 pub value: Option<String>,
753 pub selection_start: Option<usize>,
755 pub selection_end: Option<usize>,
757 pub scroll_top: Option<f32>,
759 pub scroll_left: Option<f32>,
761 pub draw: Option<Vec<DrawCmd>>,
763}
764
765impl Style {
766 pub(crate) fn overlay_delta(&mut self, delta: &Style) -> u32 {
771 let mut groups = 0u32;
772 macro_rules! merge_field {
773 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
774 $(
775 if delta.$f.is_some() {
776 self.$f = delta.$f.clone();
777 groups |= {
778 use style_groups::*;
779 $g
780 };
781 }
782 )*
783 };
784 }
785 with_style_fields!(merge_field);
786 groups
787 }
788
789 pub(crate) fn unset_field(&mut self, wire_name: &str) -> Option<u32> {
792 macro_rules! unset_match {
793 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
794 match wire_name {
795 $(
796 $name => {
797 self.$f = None;
798 Some({
799 use style_groups::*;
800 $g
801 })
802 }
803 )*
804 _ => {
805 tracing::warn!(
806 target: "bevy_react",
807 "unknown style field {wire_name:?} in styleUnset; ignoring"
808 );
809 None
810 }
811 }
812 };
813 }
814 with_style_fields!(unset_match)
815 }
816}
817
818impl Props {
819 pub fn split_events(mut self) -> (Props, UpdateEvents) {
823 let events = UpdateEvents {
824 value: self.value.take(),
825 selection_start: self.selection_start.take(),
826 selection_end: self.selection_end.take(),
827 scroll_top: self.scroll_top.take(),
828 scroll_left: self.scroll_left.take(),
829 draw: self.draw.take(),
830 };
831 (self, events)
832 }
833
834 pub fn merge_delta(
839 &mut self,
840 delta: Props,
841 unset: &[String],
842 style_unset: &[String],
843 ) -> (PropsDirty, UpdateEvents) {
844 let mut dirty = PropsDirty::default();
845 let (delta, events) = delta.split_events();
846
847 if let Some(style_delta) = &delta.style {
849 let groups = self
850 .style
851 .get_or_insert_default()
852 .overlay_delta(style_delta);
853 dirty.style.0 |= groups;
854 }
855 if delta.hover_style.is_some() {
856 self.hover_style = delta.hover_style;
857 dirty.hover_style = true;
858 }
859 if delta.press_style.is_some() {
860 self.press_style = delta.press_style;
861 dirty.press_style = true;
862 }
863 if delta.focus_style.is_some() {
864 self.focus_style = delta.focus_style;
865 dirty.focus_style = true;
866 }
867 macro_rules! merge_bool {
870 ($($f:ident => $flag:ident),* $(,)?) => {
871 $(
872 if delta.$f {
873 self.$f = true;
874 dirty.$flag = true;
875 }
876 )*
877 };
878 }
879 merge_bool!(
880 on_click => pointer,
881 on_pointer_down => pointer,
882 on_pointer_move => pointer,
883 on_pointer_up => pointer,
884 on_pointer_enter => pointer,
885 on_pointer_leave => pointer,
886 on_scroll => scroll_listener,
887 on_wheel => wheel,
888 on_change => editable_handlers,
889 on_select => editable_handlers,
890 on_focus => editable_handlers,
891 on_blur => editable_handlers,
892 flip_x => image,
893 flip_y => image,
894 );
895 if delta.multiline {
898 self.multiline = true;
899 }
900 if delta.autofocus {
901 self.autofocus = true;
902 }
903 if delta.on_resize {
906 self.on_resize = true;
907 }
908 macro_rules! merge_option {
909 ($($f:ident => $($flag:ident)?),* $(,)?) => {
910 $(
911 if delta.$f.is_some() {
912 self.$f = delta.$f;
913 $( dirty.$flag = true; )?
914 }
915 )*
916 };
917 }
918 merge_option!(
919 scroll_step => scroll_step,
920 animated => animated,
921 anchor => anchor,
922 src => image,
923 tint => image,
924 image_mode => image,
925 source_rect => image,
926 atlas => image,
927 visual_box => image,
928 target => target,
929 aria_label => aria_label,
930 max_length => , );
932
933 for name in unset {
935 match name.as_str() {
936 "style" => {
937 self.style = None;
938 dirty.style = StyleDirty::ALL;
939 }
940 "hoverStyle" => {
941 self.hover_style = None;
942 dirty.hover_style = true;
943 }
944 "pressStyle" => {
945 self.press_style = None;
946 dirty.press_style = true;
947 }
948 "focusStyle" => {
949 self.focus_style = None;
950 dirty.focus_style = true;
951 }
952 "onClick" => {
953 self.on_click = false;
954 dirty.pointer = true;
955 }
956 "onPointerDown" => {
957 self.on_pointer_down = false;
958 dirty.pointer = true;
959 }
960 "onPointerMove" => {
961 self.on_pointer_move = false;
962 dirty.pointer = true;
963 }
964 "onPointerUp" => {
965 self.on_pointer_up = false;
966 dirty.pointer = true;
967 }
968 "onPointerEnter" => {
969 self.on_pointer_enter = false;
970 dirty.pointer = true;
971 }
972 "onPointerLeave" => {
973 self.on_pointer_leave = false;
974 dirty.pointer = true;
975 }
976 "onScroll" => {
977 self.on_scroll = false;
978 dirty.scroll_listener = true;
979 }
980 "onWheel" => {
981 self.on_wheel = false;
982 dirty.wheel = true;
983 }
984 "onChange" => {
985 self.on_change = false;
986 dirty.editable_handlers = true;
987 }
988 "onSelect" => {
989 self.on_select = false;
990 dirty.editable_handlers = true;
991 }
992 "onFocus" => {
993 self.on_focus = false;
994 dirty.editable_handlers = true;
995 }
996 "onBlur" => {
997 self.on_blur = false;
998 dirty.editable_handlers = true;
999 }
1000 "flipX" => {
1001 self.flip_x = false;
1002 dirty.image = true;
1003 }
1004 "flipY" => {
1005 self.flip_y = false;
1006 dirty.image = true;
1007 }
1008 "multiline" => self.multiline = false,
1009 "autofocus" => self.autofocus = false,
1010 "onResize" => self.on_resize = false,
1011 "scrollStep" => {
1012 self.scroll_step = None;
1013 dirty.scroll_step = true;
1014 }
1015 "animated" => {
1016 self.animated = None;
1017 dirty.animated = true;
1018 }
1019 "anchor" => {
1020 self.anchor = None;
1021 dirty.anchor = true;
1022 }
1023 "src" => {
1024 self.src = None;
1025 dirty.image = true;
1026 }
1027 "tint" => {
1028 self.tint = None;
1029 dirty.image = true;
1030 }
1031 "imageMode" => {
1032 self.image_mode = None;
1033 dirty.image = true;
1034 }
1035 "sourceRect" => {
1036 self.source_rect = None;
1037 dirty.image = true;
1038 }
1039 "atlas" => {
1040 self.atlas = None;
1041 dirty.image = true;
1042 }
1043 "visualBox" => {
1044 self.visual_box = None;
1045 dirty.image = true;
1046 }
1047 "target" => {
1048 self.target = None;
1049 dirty.target = true;
1050 }
1051 "ariaLabel" => {
1052 self.aria_label = None;
1053 dirty.aria_label = true;
1054 }
1055 "maxLength" => self.max_length = None,
1056 "value" | "selectionStart" | "selectionEnd" | "scrollTop" | "scrollLeft"
1059 | "draw" => {
1060 tracing::warn!(
1061 target: "bevy_react",
1062 "event-like prop {name:?} in unset; nothing to reset"
1063 );
1064 }
1065 other => {
1066 tracing::warn!(
1067 target: "bevy_react",
1068 "unknown prop {other:?} in unset; ignoring"
1069 );
1070 }
1071 }
1072 }
1073
1074 if !style_unset.is_empty() {
1077 let style = self.style.get_or_insert_default();
1078 for name in style_unset {
1079 if let Some(groups) = style.unset_field(name) {
1080 dirty.style.0 |= groups;
1081 }
1082 }
1083 }
1084
1085 (dirty, events)
1086 }
1087}
1088
1089#[derive(Debug, Clone, Default, Deserialize)]
1091#[serde(rename_all = "camelCase")]
1092pub struct OutlineSpec {
1093 #[serde(default)]
1094 pub width: Option<Length>,
1095 #[serde(default)]
1096 pub offset: Option<Length>,
1097 #[serde(default)]
1098 pub color: Option<String>,
1099}
1100
1101#[derive(Debug, Clone, Default, Deserialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct BoxShadowSpec {
1105 #[serde(default)]
1106 pub color: Option<String>,
1107 #[serde(default)]
1108 pub x_offset: Option<Length>,
1109 #[serde(default)]
1110 pub y_offset: Option<Length>,
1111 #[serde(default)]
1112 pub spread_radius: Option<Length>,
1113 #[serde(default)]
1114 pub blur_radius: Option<Length>,
1115}
1116
1117#[derive(Debug, Clone, Deserialize)]
1119#[serde(untagged)]
1120pub enum BoxShadowList {
1121 One(BoxShadowSpec),
1122 Many(Vec<BoxShadowSpec>),
1123}
1124
1125#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
1134#[serde(rename_all = "camelCase")]
1135pub struct FilterSpec {
1136 #[serde(default)]
1138 pub blur: Option<Length>,
1139 #[serde(default)]
1141 pub brightness: Option<f32>,
1142 #[serde(default)]
1144 pub contrast: Option<f32>,
1145 #[serde(default)]
1147 pub saturate: Option<f32>,
1148 #[serde(default)]
1150 pub grayscale: Option<f32>,
1151 #[serde(default)]
1153 pub sepia: Option<f32>,
1154 #[serde(default)]
1156 pub invert: Option<f32>,
1157 #[serde(default)]
1159 pub hue_rotate: Option<Angle>,
1160}
1161
1162#[derive(Debug, Clone, Deserialize)]
1166#[serde(untagged)]
1167pub enum LineHeightSpec {
1168 Relative(f32),
1169 Px { px: f32 },
1170 Str(String),
1171}
1172
1173#[derive(Debug, Clone, Deserialize)]
1177#[serde(untagged)]
1178pub enum LetterSpacingSpec {
1179 Px(f32),
1180 Rem { rem: f32 },
1181 Str(String),
1182}
1183
1184#[derive(Debug, Clone, Default, Deserialize)]
1188#[serde(rename_all = "camelCase")]
1189pub struct TextShadowSpec {
1190 #[serde(default)]
1191 pub color: Option<String>,
1192 #[serde(default)]
1193 pub offset_x: Option<f32>,
1194 #[serde(default)]
1195 pub offset_y: Option<f32>,
1196}
1197
1198#[derive(Debug, Clone, Deserialize)]
1203#[serde(rename_all = "camelCase")]
1204pub struct GradientStop {
1205 pub color: String,
1206 #[serde(default)]
1207 pub position: Option<Length>,
1208 #[serde(default)]
1209 pub hint: Option<f32>,
1210}
1211
1212#[derive(Debug, Clone, Deserialize)]
1215#[serde(rename_all = "camelCase")]
1216pub struct AngularStop {
1217 pub color: String,
1218 #[serde(default)]
1219 pub angle: Option<Angle>,
1220 #[serde(default)]
1221 pub hint: Option<f32>,
1222}
1223
1224pub type GradientPosition = String;
1227
1228pub type ColorSpace = String;
1232
1233#[derive(Debug, Clone, Deserialize)]
1237#[serde(rename_all = "camelCase")]
1238pub enum RadialShapeSpec {
1239 Keyword(String),
1240 Circle { circle: Length },
1241 Ellipse { ellipse: [Length; 2] },
1242}
1243
1244#[derive(Debug, Clone, Default, Deserialize)]
1245#[serde(rename_all = "camelCase")]
1246pub struct LinearGradientSpec {
1247 #[serde(default)]
1250 pub angle: Option<Angle>,
1251 #[serde(default)]
1252 pub stops: Vec<GradientStop>,
1253 #[serde(default)]
1254 pub color_space: Option<ColorSpace>,
1255}
1256
1257#[derive(Debug, Clone, Default, Deserialize)]
1258#[serde(rename_all = "camelCase")]
1259pub struct RadialGradientSpec {
1260 #[serde(default)]
1261 pub position: Option<GradientPosition>,
1262 #[serde(default)]
1263 pub shape: Option<RadialShapeSpec>,
1264 #[serde(default)]
1265 pub stops: Vec<GradientStop>,
1266 #[serde(default)]
1267 pub color_space: Option<ColorSpace>,
1268}
1269
1270#[derive(Debug, Clone, Default, Deserialize)]
1271#[serde(rename_all = "camelCase")]
1272pub struct ConicGradientSpec {
1273 #[serde(default)]
1275 pub start: Option<Angle>,
1276 #[serde(default)]
1277 pub position: Option<GradientPosition>,
1278 #[serde(default)]
1279 pub stops: Vec<AngularStop>,
1280 #[serde(default)]
1281 pub color_space: Option<ColorSpace>,
1282}
1283
1284#[derive(Debug, Clone, Deserialize)]
1286#[serde(tag = "type", rename_all = "camelCase")]
1287pub enum GradientSpec {
1288 Linear(LinearGradientSpec),
1289 Radial(RadialGradientSpec),
1290 Conic(ConicGradientSpec),
1291}
1292
1293#[derive(Debug, Clone, Deserialize)]
1295#[serde(untagged)]
1296pub enum GradientList {
1297 One(GradientSpec),
1298 Many(Vec<GradientSpec>),
1299}
1300
1301#[derive(Debug, Clone, Deserialize)]
1306#[serde(untagged)]
1307pub enum ImageMode {
1308 Keyword(String),
1310 Spec(ImageModeSpec),
1311}
1312
1313#[derive(Debug, Clone, Deserialize)]
1315#[serde(tag = "type", rename_all = "camelCase")]
1316pub enum ImageModeSpec {
1317 Sliced(SliceSpec),
1318 Tiled(TiledSpec),
1319}
1320
1321#[derive(Debug, Clone, Default, Deserialize)]
1323#[serde(rename_all = "camelCase")]
1324pub struct SliceSpec {
1325 #[serde(default)]
1328 pub border: SliceBorder,
1329 #[serde(default)]
1331 pub center_scale_mode: Option<SliceScale>,
1332 #[serde(default)]
1334 pub sides_scale_mode: Option<SliceScale>,
1335 #[serde(default)]
1337 pub max_corner_scale: Option<f32>,
1338}
1339
1340#[derive(Debug, Clone, Default, Deserialize)]
1343#[serde(untagged)]
1344pub enum SliceBorder {
1345 #[default]
1347 Zero,
1348 Uniform(f32),
1350 Sides {
1352 #[serde(default)]
1353 top: f32,
1354 #[serde(default)]
1355 right: f32,
1356 #[serde(default)]
1357 bottom: f32,
1358 #[serde(default)]
1359 left: f32,
1360 },
1361}
1362
1363#[derive(Debug, Clone, Deserialize)]
1366#[serde(untagged)]
1367pub enum SliceScale {
1368 Keyword(String),
1369 Tile { tile: f32 },
1370}
1371
1372#[derive(Debug, Clone, Default, Deserialize)]
1374#[serde(rename_all = "camelCase")]
1375pub struct TiledSpec {
1376 #[serde(default)]
1377 pub tile_x: bool,
1378 #[serde(default)]
1379 pub tile_y: bool,
1380 #[serde(default)]
1382 pub stretch_value: Option<f32>,
1383}
1384
1385#[derive(Debug, Clone, Copy, Deserialize)]
1388#[serde(rename_all = "camelCase")]
1389pub struct SourceRect {
1390 pub x: f32,
1391 pub y: f32,
1392 pub width: f32,
1393 pub height: f32,
1394}
1395
1396#[derive(Debug, Clone, Deserialize)]
1401#[serde(rename_all = "camelCase")]
1402pub struct AtlasSpec {
1403 pub tile_width: u32,
1404 pub tile_height: u32,
1405 pub columns: u32,
1406 pub rows: u32,
1407 #[serde(default)]
1409 pub padding: Option<[u32; 2]>,
1410 #[serde(default)]
1412 pub offset: Option<[u32; 2]>,
1413 #[serde(default)]
1415 pub index: usize,
1416}
1417
1418#[derive(Debug, Clone, Copy, Default, PartialEq, Deserialize)]
1422#[serde(rename_all = "camelCase")]
1423pub struct Transform {
1424 pub translate_x: Option<Length>,
1427 pub translate_y: Option<Length>,
1430 pub scale: Option<f32>,
1432 pub scale_x: Option<f32>,
1433 pub scale_y: Option<f32>,
1434 pub rotate: Option<Angle>,
1436}
1437
1438#[derive(Debug, Clone, Copy, PartialEq)]
1441pub enum Length {
1442 Auto,
1443 Px(f32),
1444 Percent(f32),
1445 Vw(f32),
1446 Vh(f32),
1447 VMin(f32),
1448 VMax(f32),
1449}
1450
1451impl Default for Length {
1452 fn default() -> Self {
1453 Length::Px(0.0)
1454 }
1455}
1456
1457fn parse_length(s: &str) -> Result<Length, String> {
1459 let s = s.trim();
1460 if s.eq_ignore_ascii_case("auto") {
1461 return Ok(Length::Auto);
1462 }
1463 type LengthCtor = fn(f32) -> Length;
1466 let units: [(&str, LengthCtor); 6] = [
1467 ("px", Length::Px),
1468 ("vmin", Length::VMin),
1469 ("vmax", Length::VMax),
1470 ("vw", Length::Vw),
1471 ("vh", Length::Vh),
1472 ("%", Length::Percent),
1473 ];
1474 for (suffix, ctor) in units {
1475 if let Some(num) = s.strip_suffix(suffix) {
1476 let v: f32 = num
1477 .trim()
1478 .parse()
1479 .map_err(|_| format!("invalid length {s:?}"))?;
1480 return Ok(ctor(v));
1481 }
1482 }
1483 s.parse::<f32>()
1484 .map(Length::Px)
1485 .map_err(|_| format!("invalid length {s:?}"))
1486}
1487
1488impl<'de> Deserialize<'de> for Length {
1489 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1490 struct LengthVisitor;
1491 impl<'de> Visitor<'de> for LengthVisitor {
1492 type Value = Length;
1493 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1494 f.write_str("a number (logical pixels) or a CSS length string")
1495 }
1496 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Length, E> {
1497 Ok(Length::Px(v as f32))
1498 }
1499 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Length, E> {
1500 Ok(Length::Px(v as f32))
1501 }
1502 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Length, E> {
1503 Ok(Length::Px(v as f32))
1504 }
1505 fn visit_str<E: de::Error>(self, s: &str) -> Result<Length, E> {
1506 Ok(parse_length(s).unwrap_or_else(|e| {
1507 tracing::warn!(target: "bevy_react", "{e}; using the default");
1508 Length::default()
1509 }))
1510 }
1511 }
1512 d.deserialize_any(LengthVisitor)
1513 }
1514}
1515
1516#[derive(Debug, Clone, Copy, PartialEq, Default)]
1520pub struct Angle(f32);
1521
1522impl Angle {
1523 pub fn radians(self) -> f32 {
1525 self.0
1526 }
1527}
1528
1529fn parse_angle(s: &str) -> Result<f32, String> {
1533 use std::f32::consts::{PI, TAU};
1534 let s = s.trim();
1535 type AngleConv = fn(f32) -> f32;
1536 let units: [(&str, AngleConv); 4] = [
1537 ("deg", f32::to_radians),
1538 ("grad", |v| v * PI / 200.0),
1539 ("turn", |v| v * TAU),
1540 ("rad", |v| v),
1541 ];
1542 for (suffix, conv) in units {
1543 if let Some(num) = s.strip_suffix(suffix) {
1544 let v: f32 = num
1545 .trim()
1546 .parse()
1547 .map_err(|_| format!("invalid angle {s:?}"))?;
1548 return Ok(conv(v));
1549 }
1550 }
1551 s.parse::<f32>()
1552 .map(f32::to_radians)
1553 .map_err(|_| format!("invalid angle {s:?}"))
1554}
1555
1556impl<'de> Deserialize<'de> for Angle {
1557 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1558 struct AngleVisitor;
1559 impl Visitor<'_> for AngleVisitor {
1560 type Value = Angle;
1561 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1562 f.write_str("a number (degrees) or a CSS angle string")
1563 }
1564 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Angle, E> {
1565 Ok(Angle((v as f32).to_radians()))
1566 }
1567 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Angle, E> {
1568 Ok(Angle((v as f32).to_radians()))
1569 }
1570 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Angle, E> {
1571 Ok(Angle((v as f32).to_radians()))
1572 }
1573 fn visit_str<E: de::Error>(self, s: &str) -> Result<Angle, E> {
1574 Ok(parse_angle(s).map(Angle).unwrap_or_else(|e| {
1575 tracing::warn!(target: "bevy_react", "{e}; using the default");
1576 Angle::default()
1577 }))
1578 }
1579 }
1580 d.deserialize_any(AngleVisitor)
1581 }
1582}
1583
1584#[derive(Debug, Clone, Copy, PartialEq, Default)]
1588pub struct Time(f32);
1589
1590impl Time {
1591 pub fn from_secs(secs: f32) -> Self {
1593 Time(secs)
1594 }
1595 pub fn seconds(self) -> f32 {
1597 self.0
1598 }
1599}
1600
1601fn parse_time(s: &str) -> Result<f32, String> {
1605 let s = s.trim();
1606 if let Some(num) = s.strip_suffix("ms") {
1607 return num
1608 .trim()
1609 .parse::<f32>()
1610 .map(|v| v / 1000.0)
1611 .map_err(|_| format!("invalid time {s:?}"));
1612 }
1613 if let Some(num) = s.strip_suffix('s') {
1614 return num
1615 .trim()
1616 .parse::<f32>()
1617 .map_err(|_| format!("invalid time {s:?}"));
1618 }
1619 s.parse::<f32>()
1620 .map(|v| v / 1000.0)
1621 .map_err(|_| format!("invalid time {s:?}"))
1622}
1623
1624impl<'de> Deserialize<'de> for Time {
1625 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1626 struct TimeVisitor;
1627 impl Visitor<'_> for TimeVisitor {
1628 type Value = Time;
1629 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1630 f.write_str("a number (milliseconds) or a CSS time string")
1631 }
1632 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Time, E> {
1633 Ok(Time(v as f32 / 1000.0))
1634 }
1635 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Time, E> {
1636 Ok(Time(v as f32 / 1000.0))
1637 }
1638 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Time, E> {
1639 Ok(Time(v as f32 / 1000.0))
1640 }
1641 fn visit_str<E: de::Error>(self, s: &str) -> Result<Time, E> {
1642 Ok(parse_time(s).map(Time).unwrap_or_else(|e| {
1643 tracing::warn!(target: "bevy_react", "{e}; using the default");
1644 Time::default()
1645 }))
1646 }
1647 }
1648 d.deserialize_any(TimeVisitor)
1649 }
1650}
1651
1652#[derive(Debug, Clone, Copy, PartialEq)]
1657pub enum FontSize {
1658 Px(f32),
1659 Vw(f32),
1660 Vh(f32),
1661 VMin(f32),
1662 VMax(f32),
1663 Rem(f32),
1664}
1665
1666fn parse_font_size(s: &str) -> Result<FontSize, String> {
1670 let s = s.trim();
1671 type FsCtor = fn(f32) -> FontSize;
1672 let units: [(&str, FsCtor); 6] = [
1673 ("px", FontSize::Px),
1674 ("rem", FontSize::Rem),
1675 ("vmin", FontSize::VMin),
1676 ("vmax", FontSize::VMax),
1677 ("vw", FontSize::Vw),
1678 ("vh", FontSize::Vh),
1679 ];
1680 for (suffix, ctor) in units {
1681 if let Some(num) = s.strip_suffix(suffix) {
1682 let v: f32 = num
1683 .trim()
1684 .parse()
1685 .map_err(|_| format!("invalid fontSize {s:?}"))?;
1686 return Ok(ctor(v));
1687 }
1688 }
1689 s.parse::<f32>()
1690 .map(FontSize::Px)
1691 .map_err(|_| format!("invalid fontSize {s:?}"))
1692}
1693
1694impl<'de> Deserialize<'de> for FontSize {
1695 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1696 struct FontSizeVisitor;
1697 impl Visitor<'_> for FontSizeVisitor {
1698 type Value = FontSize;
1699 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1700 f.write_str("a number (logical pixels) or a font-size unit string")
1701 }
1702 fn visit_f64<E: de::Error>(self, v: f64) -> Result<FontSize, E> {
1703 Ok(FontSize::Px(v as f32))
1704 }
1705 fn visit_i64<E: de::Error>(self, v: i64) -> Result<FontSize, E> {
1706 Ok(FontSize::Px(v as f32))
1707 }
1708 fn visit_u64<E: de::Error>(self, v: u64) -> Result<FontSize, E> {
1709 Ok(FontSize::Px(v as f32))
1710 }
1711 fn visit_str<E: de::Error>(self, s: &str) -> Result<FontSize, E> {
1712 Ok(parse_font_size(s).unwrap_or_else(|e| {
1713 tracing::warn!(target: "bevy_react", "{e}; using the default");
1714 FontSize::Px(0.0)
1715 }))
1716 }
1717 }
1718 d.deserialize_any(FontSizeVisitor)
1719 }
1720}
1721
1722#[derive(Debug, Clone, Copy, PartialEq, Default)]
1725pub struct Rect {
1726 pub top: Length,
1727 pub right: Length,
1728 pub bottom: Length,
1729 pub left: Length,
1730}
1731
1732impl Rect {
1733 fn uniform(v: Length) -> Self {
1734 Rect {
1735 top: v,
1736 right: v,
1737 bottom: v,
1738 left: v,
1739 }
1740 }
1741
1742 fn from_shorthand(values: &[Length]) -> Result<Self, String> {
1744 Ok(match values {
1745 [a] => Rect::uniform(*a),
1746 [a, b] => Rect {
1747 top: *a,
1748 bottom: *a,
1749 right: *b,
1750 left: *b,
1751 },
1752 [a, b, c] => Rect {
1753 top: *a,
1754 right: *b,
1755 left: *b,
1756 bottom: *c,
1757 },
1758 [a, b, c, d] => Rect {
1759 top: *a,
1760 right: *b,
1761 bottom: *c,
1762 left: *d,
1763 },
1764 _ => return Err("expected 1–4 length values".into()),
1765 })
1766 }
1767}
1768
1769impl<'de> Deserialize<'de> for Rect {
1770 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1771 struct RectVisitor;
1772 impl<'de> Visitor<'de> for RectVisitor {
1773 type Value = Rect;
1774 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1775 f.write_str("a number, a CSS shorthand string, or a {top,right,bottom,left} object")
1776 }
1777 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Rect, E> {
1778 Ok(Rect::uniform(Length::Px(v as f32)))
1779 }
1780 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Rect, E> {
1781 Ok(Rect::uniform(Length::Px(v as f32)))
1782 }
1783 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Rect, E> {
1784 Ok(Rect::uniform(Length::Px(v as f32)))
1785 }
1786 fn visit_str<E: de::Error>(self, s: &str) -> Result<Rect, E> {
1787 let values: Vec<Length> = s
1790 .split_whitespace()
1791 .map(|tok| {
1792 parse_length(tok).unwrap_or_else(|e| {
1793 tracing::warn!(target: "bevy_react", "{e}; using the default");
1794 Length::default()
1795 })
1796 })
1797 .collect();
1798 Ok(Rect::from_shorthand(&values).unwrap_or_else(|e| {
1799 tracing::warn!(target: "bevy_react", "invalid rect {s:?}: {e}; using the default");
1800 Rect::default()
1801 }))
1802 }
1803 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Rect, A::Error> {
1804 let mut rect = Rect::default();
1805 while let Some(key) = map.next_key::<String>()? {
1806 let v = map.next_value::<Length>()?;
1807 match key.as_str() {
1808 "top" => rect.top = v,
1809 "right" => rect.right = v,
1810 "bottom" => rect.bottom = v,
1811 "left" => rect.left = v,
1812 _ => tracing::warn!(
1815 target: "bevy_react",
1816 "unknown rect side {key:?}; ignoring (expected top/right/bottom/left)"
1817 ),
1818 }
1819 }
1820 Ok(rect)
1821 }
1822 }
1823 d.deserialize_any(RectVisitor)
1824 }
1825}
1826
1827macro_rules! keyword_fields {
1834 ( $(
1835 $(#[$meta:meta])*
1836 fn $fn_name:ident($kind:literal) -> $ty:ty {
1837 $( $($kw:literal)|+ => $variant:ident ),+ $(,)?
1838 }
1839 )+ ) => { $(
1840 $(#[$meta])*
1841 fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
1842 struct V;
1843 impl<'de> Visitor<'de> for V {
1844 type Value = Option<$ty>;
1845 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1846 f.write_str(concat!("a `", $kind, "` keyword string"))
1847 }
1848 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
1849 Ok(Some(match s {
1850 $( $($kw)|+ => <$ty>::$variant, )+
1851 _ => {
1852 tracing::warn!(
1853 target: "bevy_react",
1854 "unrecognized {} {s:?}; using the default", $kind
1855 );
1856 <$ty>::default()
1857 }
1858 }))
1859 }
1860 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
1861 Ok(None)
1862 }
1863 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
1864 Ok(None)
1865 }
1866 }
1867 d.deserialize_any(V)
1868 }
1869 )+ };
1870}
1871
1872keyword_fields! {
1873 fn de_display("display") -> Display {
1874 "flex" => Flex, "grid" => Grid, "block" => Block, "none" => None,
1875 }
1876 fn de_box_sizing("boxSizing") -> BoxSizing {
1877 "borderBox" | "border-box" => BorderBox,
1878 "contentBox" | "content-box" => ContentBox,
1879 }
1880 fn de_position_type("positionType") -> PositionType {
1881 "absolute" => Absolute, "relative" => Relative,
1882 }
1883 fn de_overflow_axis("overflow") -> OverflowAxis {
1884 "visible" => Visible, "clip" => Clip, "hidden" => Hidden, "scroll" => Scroll,
1885 }
1886 fn de_align_items("alignItems") -> AlignItems {
1892 "start" => Start, "end" => End,
1893 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1894 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1895 }
1896 fn de_justify_items("justifyItems") -> JustifyItems {
1897 "start" => Start, "end" => End,
1898 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1899 }
1900 fn de_align_self("alignSelf") -> AlignSelf {
1901 "auto" => Auto, "start" => Start, "end" => End,
1902 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1903 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1904 }
1905 fn de_justify_self("justifySelf") -> JustifySelf {
1906 "auto" => Auto, "start" => Start, "end" => End,
1907 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1908 }
1909 fn de_align_content("alignContent") -> AlignContent {
1910 "start" => Start, "end" => End,
1911 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1912 "center" => Center, "stretch" => Stretch,
1913 "spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
1914 "spaceAround" => SpaceAround,
1915 }
1916 fn de_justify_content("justifyContent") -> JustifyContent {
1917 "start" => Start, "end" => End,
1918 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1919 "center" => Center, "stretch" => Stretch,
1920 "spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
1921 "spaceAround" => SpaceAround,
1922 }
1923 fn de_flex_direction("flexDirection") -> FlexDirection {
1924 "row" => Row, "column" => Column,
1925 "rowReverse" => RowReverse, "columnReverse" => ColumnReverse,
1926 }
1927 fn de_flex_wrap("flexWrap") -> FlexWrap {
1928 "nowrap" | "noWrap" => NoWrap, "wrap" => Wrap, "wrapReverse" => WrapReverse,
1929 }
1930 fn de_grid_auto_flow("gridAutoFlow") -> GridAutoFlow {
1931 "row" => Row, "column" => Column,
1932 "rowDense" => RowDense, "columnDense" => ColumnDense,
1933 }
1934 fn de_focus_policy("focusPolicy") -> FocusPolicy {
1937 "block" => Block, "pass" => Pass,
1938 }
1939 fn de_text_align("textAlign") -> Justify {
1940 "left" => Left, "center" => Center, "right" => Right,
1941 "justify" => Justified, "start" => Start, "end" => End,
1942 }
1943 fn de_line_break("lineBreak") -> LineBreak {
1944 "wordBoundary" => WordBoundary, "anyCharacter" => AnyCharacter,
1945 "wordOrCharacter" => WordOrCharacter, "noWrap" => NoWrap,
1946 }
1947}
1948
1949fn de_font_weight<'de, D: Deserializer<'de>>(d: D) -> Result<Option<FontWeight>, D::Error> {
1953 struct V;
1954 impl<'de> Visitor<'de> for V {
1955 type Value = Option<FontWeight>;
1956 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1957 f.write_str("a `fontWeight` keyword or numeric weight string")
1958 }
1959 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
1960 Ok(Some(match s {
1961 "thin" => FontWeight::THIN,
1962 "light" => FontWeight(300),
1963 "normal" => FontWeight::NORMAL,
1964 "medium" => FontWeight(500),
1965 "semibold" => FontWeight(600),
1966 "bold" => FontWeight::BOLD,
1967 "black" => FontWeight::BLACK,
1968 other => other.parse::<u16>().map(FontWeight).unwrap_or_else(|_| {
1969 tracing::warn!(
1970 target: "bevy_react",
1971 "unrecognized fontWeight {other:?}; using the default"
1972 );
1973 FontWeight::NORMAL
1974 }),
1975 }))
1976 }
1977 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
1978 Ok(None)
1979 }
1980 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
1981 Ok(None)
1982 }
1983 }
1984 d.deserialize_any(V)
1985}
1986
1987fn split_tracks(s: &str) -> Vec<String> {
1990 let mut out = Vec::new();
1991 let mut depth = 0usize;
1992 let mut cur = String::new();
1993 for ch in s.chars() {
1994 match ch {
1995 '(' => {
1996 depth += 1;
1997 cur.push(ch);
1998 }
1999 ')' => {
2000 depth = depth.saturating_sub(1);
2001 cur.push(ch);
2002 }
2003 c if c.is_whitespace() && depth == 0 => {
2004 if !cur.is_empty() {
2005 out.push(std::mem::take(&mut cur));
2006 }
2007 }
2008 c => cur.push(c),
2009 }
2010 }
2011 if !cur.is_empty() {
2012 out.push(cur);
2013 }
2014 out
2015}
2016
2017fn single_track(token: &str) -> Option<GridTrack> {
2020 let t = token.trim();
2021 match t {
2022 "auto" => return Some(GridTrack::auto()),
2023 "min-content" => return Some(GridTrack::min_content()),
2024 "max-content" => return Some(GridTrack::max_content()),
2025 _ => {}
2026 }
2027 let parse = |num: &str| num.trim().parse::<f32>().ok();
2028 if let Some(v) = t.strip_suffix("fr").and_then(parse) {
2029 Some(GridTrack::fr(v))
2030 } else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
2031 Some(GridTrack::flex(v))
2032 } else if let Some(v) = t.strip_suffix("px").and_then(parse) {
2033 Some(GridTrack::px(v))
2034 } else {
2035 t.strip_suffix('%').and_then(parse).map(GridTrack::percent)
2036 }
2037}
2038
2039fn repeated_track(count: u16, token: &str) -> Option<RepeatedGridTrack> {
2041 let t = token.trim();
2042 match t {
2043 "auto" => return Some(RepeatedGridTrack::auto(count)),
2044 "min-content" => return Some(RepeatedGridTrack::min_content(count)),
2045 "max-content" => return Some(RepeatedGridTrack::max_content(count)),
2046 _ => {}
2047 }
2048 let parse = |num: &str| num.trim().parse::<f32>().ok();
2049 if let Some(v) = t.strip_suffix("fr").and_then(parse) {
2050 Some(RepeatedGridTrack::fr(count, v))
2051 } else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
2052 Some(RepeatedGridTrack::flex(count, v))
2053 } else if let Some(v) = t.strip_suffix("px").and_then(parse) {
2054 Some(RepeatedGridTrack::px(count as usize, v))
2055 } else {
2056 t.strip_suffix('%')
2057 .and_then(parse)
2058 .map(|v| RepeatedGridTrack::percent(count as usize, v))
2059 }
2060}
2061
2062fn parse_template(s: &str) -> Vec<RepeatedGridTrack> {
2065 split_tracks(s)
2066 .into_iter()
2067 .filter_map(|tok| {
2068 let parse_one = || {
2069 if let Some(inner) = tok
2070 .strip_prefix("repeat(")
2071 .and_then(|t| t.strip_suffix(')'))
2072 {
2073 let (count, track) = inner.split_once(',')?;
2074 repeated_track(count.trim().parse().ok()?, track)
2075 } else {
2076 single_track(&tok).map(Into::into)
2077 }
2078 };
2079 let parsed = parse_one();
2080 if parsed.is_none() {
2081 tracing::warn!(target: "bevy_react", "ignoring unparsable grid track {tok:?}");
2082 }
2083 parsed
2084 })
2085 .collect()
2086}
2087
2088fn parse_auto_tracks(s: &str) -> Vec<GridTrack> {
2090 split_tracks(s)
2091 .iter()
2092 .filter_map(|t| {
2093 let parsed = single_track(t);
2094 if parsed.is_none() {
2095 tracing::warn!(target: "bevy_react", "ignoring unparsable grid track {t:?}");
2096 }
2097 parsed
2098 })
2099 .collect()
2100}
2101
2102fn try_grid_placement(s: &str) -> Option<GridPlacement> {
2107 enum Token {
2108 Num(i16), Span(u16), Auto,
2111 Invalid, }
2113 fn token(t: &str) -> Token {
2114 let t = t.trim();
2115 if t == "auto" {
2116 return Token::Auto;
2117 }
2118 if let Some(n) = t.strip_prefix("span") {
2119 return match n.trim().parse::<u16>() {
2120 Ok(0) | Err(_) => Token::Invalid,
2121 Ok(n) => Token::Span(n),
2122 };
2123 }
2124 match t.parse::<i16>() {
2125 Ok(0) | Err(_) => Token::Invalid,
2126 Ok(n) => Token::Num(n),
2127 }
2128 }
2129 use Token::*;
2130 if let Some((a, b)) = s.split_once('/') {
2131 return Some(match (token(a), token(b)) {
2132 (Num(start), Span(span)) => GridPlacement::start_span(start, span),
2133 (Auto, Span(span)) => GridPlacement::span(span),
2134 (Num(start), Num(end)) => GridPlacement::start_end(start, end),
2135 (Num(start), Auto) => GridPlacement::start(start),
2136 (Auto, Num(end)) => GridPlacement::end(end),
2137 (Auto, Auto) => GridPlacement::auto(),
2138 _ => return None,
2139 });
2140 }
2141 match token(s) {
2142 Auto => Some(GridPlacement::auto()),
2143 Span(span) => Some(GridPlacement::span(span)),
2144 Num(line) => Some(GridPlacement::start(line)),
2145 Invalid => None,
2146 }
2147}
2148
2149macro_rules! grid_fields {
2152 ( $(
2153 $(#[$meta:meta])*
2154 fn $fn_name:ident($expect:literal) -> $ty:ty { $parse:expr }
2155 )+ ) => { $(
2156 $(#[$meta])*
2157 fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
2158 struct V;
2159 impl<'de> Visitor<'de> for V {
2160 type Value = Option<$ty>;
2161 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2162 f.write_str($expect)
2163 }
2164 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2165 let parse: fn(&str) -> $ty = $parse;
2166 Ok(Some(parse(s)))
2167 }
2168 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
2169 Ok(None)
2170 }
2171 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
2172 Ok(None)
2173 }
2174 }
2175 d.deserialize_any(V)
2176 }
2177 )+ };
2178}
2179
2180grid_fields! {
2181 fn de_grid_template("a CSS grid template string") -> Vec<RepeatedGridTrack> {
2182 parse_template
2183 }
2184 fn de_grid_auto_tracks("a grid auto-track list string") -> Vec<GridTrack> {
2185 parse_auto_tracks
2186 }
2187 fn de_grid_placement("a grid line placement string") -> GridPlacement {
2191 |s| {
2192 try_grid_placement(s).unwrap_or_else(|| {
2193 tracing::warn!(
2194 target: "bevy_react",
2195 "unrecognized grid placement {s:?}; using the default"
2196 );
2197 GridPlacement::default()
2198 })
2199 }
2200 }
2201}
2202
2203#[derive(Debug, Clone, PartialEq, Default)]
2211pub struct BorderColorSpec {
2212 pub top: Option<String>,
2213 pub right: Option<String>,
2214 pub bottom: Option<String>,
2215 pub left: Option<String>,
2216}
2217
2218impl BorderColorSpec {
2219 fn uniform(s: String) -> Self {
2221 BorderColorSpec {
2222 top: Some(s.clone()),
2223 right: Some(s.clone()),
2224 bottom: Some(s.clone()),
2225 left: Some(s),
2226 }
2227 }
2228}
2229
2230impl<'de> Deserialize<'de> for BorderColorSpec {
2231 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2232 struct BorderColorVisitor;
2233 impl<'de> Visitor<'de> for BorderColorVisitor {
2234 type Value = BorderColorSpec;
2235 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2236 f.write_str("a CSS color string or a {top,right,bottom,left} object of colors")
2237 }
2238 fn visit_str<E: de::Error>(self, s: &str) -> Result<BorderColorSpec, E> {
2239 Ok(BorderColorSpec::uniform(s.to_owned()))
2240 }
2241 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<BorderColorSpec, A::Error> {
2242 let mut spec = BorderColorSpec::default();
2243 while let Some(key) = map.next_key::<String>()? {
2244 let v = map.next_value::<String>()?;
2245 match key.as_str() {
2246 "top" => spec.top = Some(v),
2247 "right" => spec.right = Some(v),
2248 "bottom" => spec.bottom = Some(v),
2249 "left" => spec.left = Some(v),
2250 _ => tracing::warn!(
2253 target: "bevy_react",
2254 "unknown borderColor side {key:?}; ignoring (expected top/right/bottom/left)"
2255 ),
2256 }
2257 }
2258 Ok(spec)
2259 }
2260 }
2261 d.deserialize_any(BorderColorVisitor)
2262 }
2263}
2264
2265#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2268#[serde(rename_all = "camelCase")]
2269pub struct UiEvent {
2270 pub id: NodeId,
2271 pub kind: String,
2276 #[serde(default, skip_serializing_if = "Option::is_none")]
2279 pub x: Option<f32>,
2280 #[serde(default, skip_serializing_if = "Option::is_none")]
2283 pub y: Option<f32>,
2284 #[serde(default, skip_serializing_if = "Option::is_none")]
2288 pub client_x: Option<f32>,
2289 #[serde(default, skip_serializing_if = "Option::is_none")]
2292 pub client_y: Option<f32>,
2293 #[serde(default, skip_serializing_if = "Option::is_none")]
2298 pub button: Option<u8>,
2299 #[serde(default, skip_serializing_if = "Option::is_none")]
2301 pub value: Option<String>,
2302 #[serde(default, skip_serializing_if = "Option::is_none")]
2304 pub selection_start: Option<usize>,
2305 #[serde(default, skip_serializing_if = "Option::is_none")]
2307 pub selection_end: Option<usize>,
2308 #[serde(default, skip_serializing_if = "Option::is_none")]
2311 pub selection_direction: Option<String>,
2312 #[serde(default, skip_serializing_if = "Option::is_none")]
2315 pub composing: Option<bool>,
2316 #[serde(default, skip_serializing_if = "Option::is_none")]
2319 pub scroll_top: Option<f32>,
2320 #[serde(default, skip_serializing_if = "Option::is_none")]
2323 pub scroll_left: Option<f32>,
2324 #[serde(default, skip_serializing_if = "Option::is_none")]
2327 pub delta_x: Option<f32>,
2328 #[serde(default, skip_serializing_if = "Option::is_none")]
2331 pub delta_y: Option<f32>,
2332 #[serde(default, skip_serializing_if = "Option::is_none")]
2336 pub delta_mode: Option<String>,
2337 #[serde(default, skip_serializing_if = "Option::is_none")]
2342 pub width: Option<f32>,
2343 #[serde(default, skip_serializing_if = "Option::is_none")]
2346 pub height: Option<f32>,
2347}
2348
2349#[derive(Debug, Clone, Serialize)]
2353#[serde(tag = "t", rename_all = "camelCase")]
2354pub enum Outbound {
2355 UiEvent { event: UiEvent },
2357 Event {
2360 name: String,
2361 value: serde_json::Value,
2362 },
2363 Response { id: u64, result: ResponseResult },
2365 AnimationFinished {
2369 id: crate::animations::SharedId,
2370 token: u64,
2371 finished: bool,
2372 },
2373 Reload,
2375}
2376
2377#[derive(Debug, Clone, Serialize)]
2381#[serde(tag = "status", rename_all = "camelCase")]
2382pub enum ResponseResult {
2383 Ok { value: serde_json::Value },
2384 Err { message: String },
2385}
2386
2387#[cfg(test)]
2388mod tests {
2389 use super::*;
2390
2391 #[test]
2393 fn deserializes_editable_text_create() {
2394 let json = r#"{"op":"create","id":7,"kind":"editableText","props":{
2395 "value":"hi","maxLength":40,"multiline":true,"onChange":true,
2396 "autofocus":true,"selectionStart":0,"selectionEnd":2,
2397 "ariaLabel":"Name","onSelect":true,"onFocus":true,"onBlur":true,
2398 "focusStyle":{"borderColor":"white"}}}"#;
2399 match serde_json::from_str::<Op>(json).expect("valid op") {
2400 Op::Create {
2401 id, kind, props, ..
2402 } => {
2403 assert_eq!(id, 7);
2404 assert_eq!(kind, "editableText");
2405 assert_eq!(props.value.as_deref(), Some("hi"));
2406 assert_eq!(props.max_length, Some(40));
2407 assert!(props.multiline);
2408 assert!(props.on_change);
2409 assert!(props.autofocus);
2410 assert_eq!(props.selection_start, Some(0));
2411 assert_eq!(props.selection_end, Some(2));
2412 assert_eq!(props.aria_label.as_deref(), Some("Name"));
2413 assert!(props.on_select);
2414 assert!(props.on_focus);
2415 assert!(props.on_blur);
2416 assert!(props.focus_style.is_some());
2417 }
2418 other => panic!("expected create, got {other:?}"),
2419 }
2420 }
2421
2422 #[test]
2425 fn deserializes_transform_opacity_and_transition() {
2426 let s: Style = serde_json::from_str(
2427 r#"{
2428 "transform": { "scale": 0.95, "translateX": 4, "translateY": "50%" },
2429 "opacity": 0.5,
2430 "transition": { "transform": { "duration": 0.15, "easing": "easeOut" } }
2431 }"#,
2432 )
2433 .expect("style decodes");
2434 let t = s.transform.expect("transform present");
2435 assert_eq!(t.scale, Some(0.95));
2436 assert_eq!(t.translate_x, Some(Length::Px(4.0)));
2438 assert_eq!(t.translate_y, Some(Length::Percent(50.0)));
2439 assert_eq!(t.scale_x, None);
2440 assert_eq!(s.opacity, Some(0.5));
2441 let transition = s.transition.expect("transition present");
2442 assert!(transition.for_transform().is_some());
2443 assert!(transition.for_opacity().is_none());
2444 }
2445
2446 #[test]
2449 fn angle_units() {
2450 use std::f32::consts::{PI, TAU};
2451 let parse = |v: serde_json::Value| serde_json::from_value::<Angle>(v).unwrap().radians();
2452 assert!((parse(serde_json::json!(180)) - PI).abs() < 1e-5);
2453 assert!((parse(serde_json::json!("180deg")) - PI).abs() < 1e-5);
2454 assert!((parse(serde_json::json!("3.14159rad")) - PI).abs() < 1e-4);
2455 assert!((parse(serde_json::json!("0.5turn")) - PI).abs() < 1e-5);
2456 assert!((parse(serde_json::json!("400grad")) - TAU).abs() < 1e-5);
2457 }
2458
2459 #[test]
2462 fn border_color_scalar_and_per_side() {
2463 let uniform: Style =
2465 serde_json::from_str(r#"{ "borderColor": "white" }"#).expect("scalar decodes");
2466 let bc = uniform.border_color.expect("border_color present");
2467 assert_eq!(bc.top.as_deref(), Some("white"));
2468 assert_eq!(bc.right.as_deref(), Some("white"));
2469 assert_eq!(bc.bottom.as_deref(), Some("white"));
2470 assert_eq!(bc.left.as_deref(), Some("white"));
2471
2472 let sided: Style =
2474 serde_json::from_str(r##"{ "borderColor": { "top": "#f00", "left": "blue" } }"##)
2475 .expect("object decodes");
2476 let bc = sided.border_color.expect("border_color present");
2477 assert_eq!(bc.top.as_deref(), Some("#f00"));
2478 assert_eq!(bc.left.as_deref(), Some("blue"));
2479 assert_eq!(bc.right, None);
2480 assert_eq!(bc.bottom, None);
2481
2482 let bogus: Style =
2486 serde_json::from_str(r#"{ "borderColor": { "middle": "red", "top": "blue" } }"#)
2487 .expect("unknown side key must not abort deserialization");
2488 let bc = bogus.border_color.expect("border_color present");
2489 assert_eq!(bc.top.as_deref(), Some("blue"));
2490 assert_eq!(bc.right, None);
2491 assert_eq!(bc.bottom, None);
2492 assert_eq!(bc.left, None);
2493 }
2494
2495 #[test]
2499 fn bad_unit_values_fall_back_instead_of_aborting() {
2500 let s: Style = serde_json::from_str(r#"{ "width": "100pixels", "height": "40px" }"#)
2502 .expect("a bad length must not abort deserialization");
2503 assert_eq!(s.width, Some(Length::default()));
2504 assert_eq!(s.height, Some(Length::Px(40.0)));
2505
2506 let s: Style = serde_json::from_str(r#"{ "fontSize": "16pxx" }"#)
2508 .expect("bad fontSize must not abort");
2509 assert_eq!(s.font_size, Some(FontSize::Px(0.0)));
2510
2511 let t: Transform = serde_json::from_str(r#"{ "rotate": "45degg", "translateX": "50%" }"#)
2513 .expect("bad angle must not abort");
2514 assert_eq!(t.rotate, Some(Angle::default()));
2515 assert_eq!(t.translate_x, Some(Length::Percent(50.0)));
2516
2517 let s: Style =
2521 serde_json::from_str(r#"{ "padding": "16asd" }"#).expect("bad rect must not abort");
2522 assert_eq!(s.padding, Some(Rect::default()));
2523
2524 let s: Style = serde_json::from_str(r#"{ "padding": "8px 16asd" }"#)
2525 .expect("partial-bad rect must not abort");
2526 assert_eq!(
2528 s.padding,
2529 Some(Rect {
2530 top: Length::Px(8.0),
2531 bottom: Length::Px(8.0),
2532 right: Length::default(),
2533 left: Length::default(),
2534 })
2535 );
2536
2537 let s: Style = serde_json::from_str(r#"{ "padding": "8px 16px" }"#)
2538 .expect("valid two-value shorthand decodes");
2539 assert_eq!(
2540 s.padding,
2541 Some(Rect {
2542 top: Length::Px(8.0),
2543 bottom: Length::Px(8.0),
2544 right: Length::Px(16.0),
2545 left: Length::Px(16.0),
2546 })
2547 );
2548
2549 let s: Style = serde_json::from_str(r#"{ "padding": "1px 2px 3px 4px 5px" }"#)
2551 .expect("bad value-count must not abort");
2552 assert_eq!(s.padding, Some(Rect::default()));
2553 }
2554
2555 #[test]
2561 fn keyword_fields_decode_to_bevy_enums() {
2562 let s: Style = serde_json::from_value(serde_json::json!({
2563 "display": "grid",
2564 "alignItems": "start",
2565 "alignSelf": "flexStart",
2566 "alignContent": "spaceBetween",
2567 "justifyContent": "flexEnd",
2568 "flexWrap": "nowrap",
2569 "focusPolicy": "block",
2570 "textAlign": "justify",
2571 "lineBreak": "anyCharacter",
2572 }))
2573 .expect("keyword style decodes");
2574 assert_eq!(s.display, Some(Display::Grid));
2575 assert_eq!(s.align_items, Some(AlignItems::Start));
2576 assert_eq!(s.align_self, Some(AlignSelf::FlexStart));
2577 assert_eq!(s.align_content, Some(AlignContent::SpaceBetween));
2578 assert_eq!(s.justify_content, Some(JustifyContent::FlexEnd));
2579 assert_eq!(s.flex_wrap, Some(FlexWrap::NoWrap));
2580 assert_eq!(s.focus_policy, Some(FocusPolicy::Block));
2581 assert_eq!(s.text_align, Some(Justify::Justified));
2582 assert_eq!(s.line_break, Some(LineBreak::AnyCharacter));
2583
2584 let s: Style = serde_json::from_value(serde_json::json!({
2585 "alignItems": "flexStart",
2586 "justifyContent": "start",
2587 "boxSizing": "border-box",
2589 "flexWrap": "noWrap",
2590 }))
2591 .expect("alias keywords decode");
2592 assert_eq!(s.align_items, Some(AlignItems::FlexStart));
2593 assert_eq!(s.justify_content, Some(JustifyContent::Start));
2594 assert_eq!(s.box_sizing, Some(BoxSizing::BorderBox));
2595 assert_eq!(s.flex_wrap, Some(FlexWrap::NoWrap));
2596 }
2597
2598 #[test]
2602 fn unknown_enum_keywords_fall_back_to_default() {
2603 let s: Style = serde_json::from_value(serde_json::json!({
2604 "display": "flx",
2605 "alignItems": "centre",
2606 "flexDirection": "sideways",
2607 "textAlign": "middle",
2608 "fontWeight": "heavyish",
2609 "focusPolicy": "weird",
2610 "lineBreak": "wordBoundary",
2612 }))
2613 .expect("bad keywords must not abort deserialization");
2614 assert_eq!(s.display, Some(Display::default()));
2615 assert_eq!(s.align_items, Some(AlignItems::default()));
2616 assert_eq!(s.flex_direction, Some(FlexDirection::default()));
2617 assert_eq!(s.text_align, Some(Justify::default()));
2618 assert_eq!(s.font_weight, Some(FontWeight::NORMAL));
2619 assert_eq!(s.focus_policy, Some(FocusPolicy::Pass));
2620 assert_eq!(s.line_break, Some(LineBreak::WordBoundary));
2621 }
2622
2623 #[test]
2625 fn font_weight_keywords_and_numeric() {
2626 let fw = |v: serde_json::Value| {
2627 serde_json::from_value::<Style>(serde_json::json!({ "fontWeight": v }))
2628 .expect("fontWeight decodes")
2629 .font_weight
2630 };
2631 assert_eq!(fw("bold".into()), Some(FontWeight::BOLD));
2632 assert_eq!(fw("600".into()), Some(FontWeight(600)));
2633 assert_eq!(fw("thin".into()), Some(FontWeight::THIN));
2634 }
2635
2636 #[test]
2638 fn grid_templates_and_placement_decode() {
2639 let s: Style = serde_json::from_value(serde_json::json!({
2640 "gridTemplateColumns": "1fr 2fr 100px",
2641 "gridTemplateRows": "repeat(3, 1fr)",
2642 "gridAutoRows": "auto 40px",
2643 }))
2644 .expect("grid template decodes");
2645 assert_eq!(s.grid_template_columns.map(|t| t.len()), Some(3));
2646 assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(1));
2647 assert_eq!(s.grid_auto_rows.map(|t| t.len()), Some(2));
2648
2649 let s: Style =
2651 serde_json::from_value(serde_json::json!({ "gridTemplateRows": "1fr bogus 2fr" }))
2652 .expect("bad track must not abort");
2653 assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(2));
2654
2655 let placed = |v: &str| {
2656 let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
2657 .expect("grid placement decodes");
2658 format!("{:?}", s.grid_row.unwrap())
2659 };
2660 let expect = |p: GridPlacement| format!("{p:?}");
2661 assert_eq!(placed("1 / 3"), expect(GridPlacement::start_end(1, 3)));
2662 assert_eq!(placed("span 2"), expect(GridPlacement::span(2)));
2663 assert_eq!(
2664 placed("2 / span 3"),
2665 expect(GridPlacement::start_span(2, 3))
2666 );
2667 assert_eq!(placed("2 / 2"), expect(GridPlacement::start_end(2, 2)));
2668 assert_eq!(placed("-1"), expect(GridPlacement::start(-1)));
2669 assert_eq!(placed("2 / auto"), expect(GridPlacement::start(2)));
2670 assert_eq!(placed("auto / 3"), expect(GridPlacement::end(3)));
2671 }
2672
2673 #[test]
2677 fn grid_placement_zero_falls_back_to_auto() {
2678 let placed = |v: &str| {
2679 let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
2680 .expect("zero placement must not abort");
2681 format!("{:?}", s.grid_row.unwrap())
2682 };
2683 let auto = format!("{:?}", GridPlacement::auto());
2684 for s in ["0", "span 0", "0 / 2", "2 / 0", "0 / span 2", "2 / span 0"] {
2685 assert_eq!(placed(s), auto, "input {s:?}");
2686 }
2687 assert_eq!(placed("garbage"), auto);
2689 }
2690
2691 #[test]
2695 fn deserializes_filter_functions() {
2696 let s: Style = serde_json::from_str(
2697 r#"{ "filter": {
2698 "blur": "4px", "brightness": 1.2, "grayscale": 1,
2699 "saturate": 0.5, "hueRotate": 90
2700 } }"#,
2701 )
2702 .expect("filter decodes");
2703 let f = s.filter.expect("filter present");
2704 assert_eq!(f.blur, Some(Length::Px(4.0)));
2705 assert_eq!(f.brightness, Some(1.2));
2706 assert_eq!(f.grayscale, Some(1.0));
2707 assert_eq!(f.saturate, Some(0.5));
2708 assert!((f.hue_rotate.unwrap().radians() - std::f32::consts::FRAC_PI_2).abs() < 1e-5);
2709 assert_eq!(f.contrast, None);
2711 assert_eq!(f.sepia, None);
2712 assert_eq!(f.invert, None);
2713
2714 let s: Style = serde_json::from_str(r#"{ "filter": { "blur": "4pxx" }, "opacity": 0.5 }"#)
2716 .expect("a bad filter unit must not abort the style");
2717 assert_eq!(s.filter.unwrap().blur, Some(Length::default()));
2718 assert_eq!(s.opacity, Some(0.5));
2719 }
2720
2721 #[test]
2724 fn serializes_change_event_with_value() {
2725 let ev = UiEvent {
2726 id: 7,
2727 kind: "change".into(),
2728 value: Some("hello".into()),
2729 ..Default::default()
2730 };
2731 let v = serde_json::to_value(&ev).expect("serializable");
2732 assert_eq!(v["kind"], "change");
2733 assert_eq!(v["value"], "hello");
2734 assert!(v.get("clientX").is_none(), "pointer fields omitted");
2735 assert!(v.get("button").is_none(), "button omitted on text events");
2736 }
2737
2738 #[test]
2741 fn serializes_pointer_event_with_button() {
2742 let ev = UiEvent {
2743 id: 3,
2744 kind: "pointerDown".into(),
2745 button: Some(2),
2746 ..Default::default()
2747 };
2748 let v = serde_json::to_value(&ev).expect("serializable");
2749 assert_eq!(v["kind"], "pointerDown");
2750 assert_eq!(v["button"], 2);
2751 }
2752
2753 #[test]
2757 fn style_field_table_is_complete() {
2758 macro_rules! build_full {
2759 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
2760 Style { $($f: None,)* }
2761 };
2762 }
2763 let _style: Style = with_style_fields!(build_full);
2764 }
2765
2766 #[test]
2770 fn style_wire_names_match_serde_rename() {
2771 fn camel(s: &str) -> String {
2772 let mut out = String::new();
2773 let mut up = false;
2774 for c in s.chars() {
2775 if c == '_' {
2776 up = true;
2777 } else if up {
2778 out.extend(c.to_uppercase());
2779 up = false;
2780 } else {
2781 out.push(c);
2782 }
2783 }
2784 out
2785 }
2786 macro_rules! check {
2787 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
2788 $( assert_eq!(camel(stringify!($f)), $name, "table wire name for `{}`", stringify!($f)); )*
2789 };
2790 }
2791 with_style_fields!(check);
2792 }
2793
2794 fn props(json: serde_json::Value) -> Props {
2795 serde_json::from_value(json).expect("valid props")
2796 }
2797
2798 #[test]
2800 fn merge_delta_sets_and_preserves() {
2801 let mut cached = props(serde_json::json!({
2802 "style": { "backgroundColor": "red", "outline": { "color": "white" } },
2803 "hoverStyle": { "backgroundColor": "blue" },
2804 "onClick": true,
2805 "src": "a.png",
2806 }));
2807 let (dirty, ev) = cached.merge_delta(
2808 props(serde_json::json!({ "style": { "width": 100 } })),
2809 &[],
2810 &[],
2811 );
2812
2813 let style = cached.style.as_ref().unwrap();
2814 assert_eq!(style.width, Some(Length::Px(100.0)));
2815 assert_eq!(style.background_color.as_deref(), Some("red"));
2816 assert!(style.outline.is_some(), "untouched style fields preserved");
2817 assert!(cached.hover_style.is_some(), "untouched props preserved");
2818 assert!(cached.on_click);
2819 assert_eq!(cached.src.as_deref(), Some("a.png"));
2820
2821 assert!(dirty.style.intersects(style_groups::LAYOUT));
2822 assert!(
2823 !dirty
2824 .style
2825 .intersects(style_groups::BACKGROUND | style_groups::OUTLINE),
2826 "untouched groups must stay clean"
2827 );
2828 assert!(!dirty.hover_style && !dirty.pointer && !dirty.image);
2829 assert!(dirty.style.intersects(style_groups::TRANSITION));
2831 assert!(ev.value.is_none() && ev.draw.is_none());
2832 }
2833
2834 #[test]
2837 fn merge_delta_unsets() {
2838 let mut cached = props(serde_json::json!({
2839 "style": { "backgroundColor": "red", "width": 50 },
2840 "hoverStyle": { "backgroundColor": "blue" },
2841 "onClick": true,
2842 }));
2843 let (dirty, _) = cached.merge_delta(
2844 Props::default(),
2845 &["hoverStyle".into(), "onClick".into()],
2846 &["backgroundColor".into()],
2847 );
2848
2849 let style = cached.style.as_ref().unwrap();
2850 assert_eq!(style.background_color, None);
2851 assert_eq!(
2852 style.width,
2853 Some(Length::Px(50.0)),
2854 "other style fields kept"
2855 );
2856 assert!(cached.hover_style.is_none());
2857 assert!(!cached.on_click);
2858 assert!(dirty.style.intersects(style_groups::BACKGROUND));
2859 assert!(!dirty.style.intersects(style_groups::LAYOUT));
2860 assert!(dirty.hover_style && dirty.pointer);
2861 assert!(dirty.any_style_variant());
2862 }
2863
2864 #[test]
2866 fn merge_delta_unsets_style_wholesale() {
2867 let mut cached = props(serde_json::json!({
2868 "style": { "backgroundColor": "red", "width": 50 },
2869 }));
2870 let (dirty, _) = cached.merge_delta(Props::default(), &["style".into()], &[]);
2871 assert!(cached.style.is_none());
2872 assert_eq!(dirty.style, StyleDirty::ALL);
2873 }
2874
2875 #[test]
2877 fn merge_delta_events_not_cached() {
2878 let mut cached = Props::default();
2879 let (dirty, ev) = cached.merge_delta(
2880 props(serde_json::json!({
2881 "value": "hi", "selectionStart": 1, "selectionEnd": 3,
2882 "scrollTop": 40.0, "scrollLeft": 2.0,
2883 })),
2884 &[],
2885 &[],
2886 );
2887 assert_eq!(ev.value.as_deref(), Some("hi"));
2888 assert_eq!((ev.selection_start, ev.selection_end), (Some(1), Some(3)));
2889 assert_eq!((ev.scroll_top, ev.scroll_left), (Some(40.0), Some(2.0)));
2890 assert!(cached.value.is_none() && cached.scroll_top.is_none());
2891 assert!(cached.selection_start.is_none());
2892 assert!(!dirty.style.any() && !dirty.image && !dirty.anchor);
2894 }
2895
2896 #[test]
2899 fn merge_delta_replaces_variants_atomically() {
2900 let mut cached = props(serde_json::json!({
2901 "hoverStyle": { "backgroundColor": "blue", "width": 10 },
2902 }));
2903 let (dirty, _) = cached.merge_delta(
2904 props(serde_json::json!({ "hoverStyle": { "outline": { "color": "white" } } })),
2905 &[],
2906 &[],
2907 );
2908 let hover = cached.hover_style.as_ref().unwrap();
2909 assert!(hover.outline.is_some());
2910 assert_eq!(hover.background_color, None, "atomic replace, not a merge");
2911 assert_eq!(hover.width, None);
2912 assert!(dirty.hover_style);
2913 }
2914
2915 #[test]
2918 fn merge_delta_ignores_unknown_names() {
2919 let mut cached = props(serde_json::json!({ "style": { "width": 10 } }));
2920 let (dirty, _) = cached.merge_delta(
2921 Props::default(),
2922 &["nope".into(), "value".into()],
2923 &["alsoNope".into()],
2924 );
2925 assert_eq!(cached.style.as_ref().unwrap().width, Some(Length::Px(10.0)));
2926 assert!(!dirty.style.any());
2927 }
2928
2929 #[test]
2931 fn merge_delta_converges() {
2932 let base = serde_json::json!({
2933 "style": { "backgroundColor": "red", "width": 10 }, "onClick": true,
2934 });
2935 let mut two_steps = props(base.clone());
2936 two_steps.merge_delta(
2937 props(serde_json::json!({ "style": { "width": 20 } })),
2938 &[],
2939 &[],
2940 );
2941 two_steps.merge_delta(
2942 props(serde_json::json!({ "style": { "height": 5 } })),
2943 &[],
2944 &["backgroundColor".into()],
2945 );
2946
2947 let mut one_step = props(base);
2948 one_step.merge_delta(
2949 props(serde_json::json!({ "style": { "width": 20, "height": 5 } })),
2950 &[],
2951 &["backgroundColor".into()],
2952 );
2953
2954 let a = two_steps.style.as_ref().unwrap();
2955 let b = one_step.style.as_ref().unwrap();
2956 assert_eq!(a.width, b.width);
2957 assert_eq!(a.height, b.height);
2958 assert_eq!(a.background_color, b.background_color);
2959 assert!(two_steps.on_click && one_step.on_click);
2960 }
2961
2962 #[test]
2964 fn split_events_strips_event_fields() {
2965 let full = props(serde_json::json!({
2966 "style": { "width": 10 }, "onClick": true, "value": "v",
2967 "selectionStart": 0, "selectionEnd": 1, "scrollTop": 5.0,
2968 }));
2969 let (state, ev) = full.split_events();
2970 assert!(state.style.is_some() && state.on_click);
2971 assert!(state.value.is_none() && state.selection_start.is_none());
2972 assert!(state.scroll_top.is_none());
2973 assert_eq!(ev.value.as_deref(), Some("v"));
2974 assert_eq!(ev.scroll_top, Some(5.0));
2975 }
2976
2977 #[test]
2981 fn deserializes_update_delta_form() {
2982 let minimal: Op = serde_json::from_str(r#"{"op":"update","id":3,"props":{}}"#).unwrap();
2983 match minimal {
2984 Op::Update {
2985 unset, style_unset, ..
2986 } => {
2987 assert!(unset.is_empty() && style_unset.is_empty());
2988 }
2989 other => panic!("expected update, got {other:?}"),
2990 }
2991 let full: Op = serde_json::from_str(
2992 r#"{"op":"update","id":3,"props":{"style":{"width":1}},
2993 "unset":["onClick"],"styleUnset":["backgroundColor"]}"#,
2994 )
2995 .unwrap();
2996 match full {
2997 Op::Update {
2998 unset, style_unset, ..
2999 } => {
3000 assert_eq!(unset, vec!["onClick"]);
3001 assert_eq!(style_unset, vec!["backgroundColor"]);
3002 }
3003 other => panic!("expected update, got {other:?}"),
3004 }
3005 }
3006
3007 #[test]
3011 fn deserializes_draw_op() {
3012 let op: Op = serde_json::from_str(
3013 r##"{"op":"draw","id":7,"cmds":[
3014 {"cmd":"clear"},
3015 {"cmd":"clearRect","x":1.0,"y":2.0,"w":3.0,"h":4.0},
3016 {"cmd":"fillStyle","color":"#f00"}
3017 ]}"##,
3018 )
3019 .unwrap();
3020 match op {
3021 Op::Draw { id, cmds } => {
3022 assert_eq!(id, 7);
3023 assert_eq!(cmds.len(), 3);
3024 assert_eq!(cmds[0], DrawCmd::Clear);
3025 assert_eq!(
3026 cmds[1],
3027 DrawCmd::ClearRect {
3028 x: 1.0,
3029 y: 2.0,
3030 w: 3.0,
3031 h: 4.0
3032 }
3033 );
3034 assert_eq!(
3035 cmds[2],
3036 DrawCmd::FillStyle {
3037 color: "#f00".into()
3038 }
3039 );
3040 }
3041 other => panic!("expected draw, got {other:?}"),
3042 }
3043 }
3044
3045 #[test]
3048 fn serializes_resize_ui_event() {
3049 let v = serde_json::to_value(Outbound::UiEvent {
3050 event: UiEvent {
3051 id: 5,
3052 kind: "resize".into(),
3053 width: Some(300.0),
3054 height: Some(150.0),
3055 ..Default::default()
3056 },
3057 })
3058 .unwrap();
3059 assert_eq!(v["t"], "uiEvent");
3060 let ev = &v["event"];
3061 assert_eq!(ev["id"], 5);
3062 assert_eq!(ev["kind"], "resize");
3063 assert_eq!(ev["width"], 300.0);
3064 assert_eq!(ev["height"], 150.0);
3065 assert!(ev.get("x").is_none() && ev.get("scrollTop").is_none());
3066 }
3067
3068 #[test]
3071 fn merge_delta_on_resize_flag() {
3072 let mut cached = Props::default();
3073 let (dirty, _) =
3074 cached.merge_delta(props(serde_json::json!({ "onResize": true })), &[], &[]);
3075 assert!(cached.on_resize);
3076 assert!(!dirty.pointer && !dirty.scroll_listener);
3077 cached.merge_delta(Props::default(), &["onResize".into()], &[]);
3078 assert!(!cached.on_resize);
3079 }
3080
3081 #[test]
3084 fn merge_delta_wheel_flag() {
3085 let mut cached = Props::default();
3086 let (dirty, _) =
3087 cached.merge_delta(props(serde_json::json!({ "onWheel": true })), &[], &[]);
3088 assert!(cached.on_wheel);
3089 assert!(dirty.wheel);
3090 assert!(!dirty.pointer && !dirty.scroll_listener);
3091
3092 let (dirty, _) = cached.merge_delta(Props::default(), &["onWheel".into()], &[]);
3093 assert!(!cached.on_wheel);
3094 assert!(dirty.wheel);
3095 }
3096
3097 #[test]
3100 fn deserializes_cursor_name() {
3101 let s: Style = serde_json::from_str(r#"{ "cursor": "pointer" }"#).expect("cursor decodes");
3102 assert_eq!(s.cursor.as_deref(), Some("pointer"));
3103
3104 let s: Style =
3105 serde_json::from_str(r#"{ "cursor": "hand" }"#).expect("custom name decodes");
3106 assert_eq!(s.cursor.as_deref(), Some("hand"));
3107 }
3108
3109 #[test]
3112 fn merge_delta_cursor_group() {
3113 let mut cached = Props::default();
3114 let (dirty, _) = cached.merge_delta(
3115 props(serde_json::json!({ "style": { "cursor": "pointer" } })),
3116 &[],
3117 &[],
3118 );
3119 assert_eq!(
3120 cached.style.as_ref().unwrap().cursor.as_deref(),
3121 Some("pointer")
3122 );
3123 assert!(dirty.style.intersects(style_groups::CURSOR));
3124 assert!(!dirty.style.intersects(style_groups::LAYOUT));
3125
3126 let (dirty, _) = cached.merge_delta(Props::default(), &[], &["cursor".into()]);
3127 assert_eq!(cached.style.as_ref().unwrap().cursor, None);
3128 assert!(dirty.style.intersects(style_groups::CURSOR));
3129 }
3130}