1use std::fmt;
24
25use bevy::text::{FontWeight, Justify, LineBreak};
26use bevy::ui::{
27 AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, FocusPolicy,
28 GridAutoFlow, GridPlacement, GridTrack, JustifyContent, JustifyItems, JustifySelf,
29 OverflowAxis, PositionType, RepeatedGridTrack,
30};
31use serde::de::{self, Deserializer, MapAccess, Visitor};
32use serde::{Deserialize, Serialize};
33
34pub type NodeId = u32;
37
38pub const ROOT_ID: NodeId = 0;
39
40#[derive(Debug, Clone, Deserialize)]
44#[serde(tag = "op", rename_all = "camelCase")]
45pub enum Op {
46 Reset,
49 Create {
51 id: NodeId,
52 kind: String,
53 #[serde(default)]
54 props: Props,
55 #[serde(default)]
58 text: Option<String>,
59 },
60 CreateText { id: NodeId, text: String },
62 CreateTextSpan { id: NodeId, text: String },
65 Append { parent: NodeId, child: NodeId },
67 Insert {
69 parent: NodeId,
70 child: NodeId,
71 before: NodeId,
72 },
73 Remove { parent: NodeId, child: NodeId },
75 Update {
91 id: NodeId,
92 #[serde(default)]
93 props: Props,
94 #[serde(default)]
96 unset: Vec<String>,
97 #[serde(default, rename = "styleUnset")]
101 style_unset: Vec<String>,
102 },
103 UpdateText { id: NodeId, text: String },
105 Draw { id: NodeId, cmds: Vec<DrawCmd> },
113}
114
115pub(crate) fn decode_warn(kind: &'static str, value: &str, message: &str) {
121 tracing::warn!(target: "bevy_react", "{message}");
122 crate::diag::decode_report(kind, value, message);
123}
124
125pub struct OpBatch(pub Vec<Op>);
132
133fn op_target_id(op: &Op) -> Option<NodeId> {
136 match op {
137 Op::Create { id, .. }
138 | Op::CreateText { id, .. }
139 | Op::CreateTextSpan { id, .. }
140 | Op::Update { id, .. }
141 | Op::UpdateText { id, .. }
142 | Op::Draw { id, .. } => Some(*id),
143 Op::Reset | Op::Append { .. } | Op::Insert { .. } | Op::Remove { .. } => None,
144 }
145}
146
147impl<'de> Deserialize<'de> for OpBatch {
148 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
149 struct BatchVisitor;
150 impl<'de> Visitor<'de> for BatchVisitor {
151 type Value = Vec<Op>;
152 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
153 f.write_str("an array of reconciler ops")
154 }
155 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<Op>, A::Error> {
156 crate::diag::decode_batch_start();
160 let mut ops = Vec::with_capacity(seq.size_hint().unwrap_or(0));
161 loop {
162 let mark = crate::diag::decode_watermark();
163 let Some(op) = seq.next_element::<Op>()? else {
164 break;
165 };
166 crate::diag::decode_attribute_since(mark, op_target_id(&op));
167 ops.push(op);
168 }
169 Ok(ops)
170 }
171 }
172 d.deserialize_seq(BatchVisitor).map(OpBatch)
173 }
174}
175
176#[derive(Debug, Clone, Default, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct Props {
183 #[serde(default)]
185 pub style: Option<Style>,
186 #[serde(default)]
189 pub hover_style: Option<Style>,
190 #[serde(default)]
192 pub press_style: Option<Style>,
193 #[serde(default)]
197 pub focus_style: Option<Style>,
198 #[serde(default)]
200 pub on_click: bool,
201 #[serde(default)]
203 pub on_pointer_down: bool,
204 #[serde(default)]
207 pub on_pointer_move: bool,
208 #[serde(default)]
210 pub on_pointer_up: bool,
211 #[serde(default)]
214 pub on_pointer_enter: bool,
215 #[serde(default)]
218 pub on_pointer_leave: bool,
219
220 #[serde(default)]
226 pub scroll_top: Option<f32>,
227 #[serde(default)]
229 pub scroll_left: Option<f32>,
230 #[serde(default)]
234 pub scroll_step: Option<f32>,
235 #[serde(default)]
240 pub on_scroll: bool,
241 #[serde(default)]
246 pub on_wheel: bool,
247
248 #[serde(default)]
253 pub animated: Option<crate::animations::AnimatedBindings>,
254 #[serde(default)]
258 pub anchor: Option<crate::anchor::Anchor>,
259
260 #[serde(default)]
264 pub src: Option<String>,
265 #[serde(default)]
267 pub tint: Option<String>,
268 #[serde(default)]
270 pub flip_x: bool,
271 #[serde(default)]
273 pub flip_y: bool,
274 #[serde(default)]
277 pub image_mode: Option<ImageMode>,
278 #[serde(default)]
282 pub source_rect: Option<SourceRect>,
283 #[serde(default)]
286 pub atlas: Option<AtlasSpec>,
287 #[serde(default)]
290 pub visual_box: Option<String>,
291
292 #[serde(default)]
300 pub draw: Option<Vec<DrawCmd>>,
301 #[serde(default)]
306 pub on_resize: bool,
307
308 #[serde(default)]
314 pub target: Option<String>,
315
316 #[serde(default)]
321 pub value: Option<String>,
322 #[serde(default)]
324 pub max_length: Option<usize>,
325 #[serde(default)]
327 pub multiline: bool,
328 #[serde(default)]
330 pub on_change: bool,
331 #[serde(default)]
333 pub autofocus: bool,
334 #[serde(default)]
338 pub selection_start: Option<usize>,
339 #[serde(default)]
341 pub selection_end: Option<usize>,
342 #[serde(default)]
344 pub aria_label: Option<String>,
345 #[serde(default)]
347 pub on_select: bool,
348 #[serde(default)]
350 pub on_focus: bool,
351 #[serde(default)]
353 pub on_blur: bool,
354}
355
356pub use crate::canvas::DrawCmd;
360
361#[derive(Debug, Clone, Default, Deserialize)]
373#[serde(rename_all = "camelCase")]
374pub struct Style {
375 #[serde(default, deserialize_with = "de_display")]
377 pub display: Option<Display>,
378 #[serde(default, deserialize_with = "de_box_sizing")]
379 pub box_sizing: Option<BoxSizing>,
380 #[serde(default, deserialize_with = "de_position_type")]
381 pub position_type: Option<PositionType>,
382 #[serde(default, deserialize_with = "de_overflow_axis")]
383 pub overflow_x: Option<OverflowAxis>,
384 #[serde(default, deserialize_with = "de_overflow_axis")]
385 pub overflow_y: Option<OverflowAxis>,
386 #[serde(default)]
387 pub scrollbar_width: Option<f32>,
388
389 #[serde(default)]
391 pub left: Option<Length>,
392 #[serde(default)]
393 pub right: Option<Length>,
394 #[serde(default)]
395 pub top: Option<Length>,
396 #[serde(default)]
397 pub bottom: Option<Length>,
398
399 #[serde(default)]
401 pub width: Option<Length>,
402 #[serde(default)]
403 pub height: Option<Length>,
404 #[serde(default)]
405 pub min_width: Option<Length>,
406 #[serde(default)]
407 pub min_height: Option<Length>,
408 #[serde(default)]
409 pub max_width: Option<Length>,
410 #[serde(default)]
411 pub max_height: Option<Length>,
412 #[serde(default)]
413 pub aspect_ratio: Option<f32>,
414
415 #[serde(default, deserialize_with = "de_align_items")]
417 pub align_items: Option<AlignItems>,
418 #[serde(default, deserialize_with = "de_justify_items")]
419 pub justify_items: Option<JustifyItems>,
420 #[serde(default, deserialize_with = "de_align_self")]
421 pub align_self: Option<AlignSelf>,
422 #[serde(default, deserialize_with = "de_justify_self")]
423 pub justify_self: Option<JustifySelf>,
424 #[serde(default, deserialize_with = "de_align_content")]
425 pub align_content: Option<AlignContent>,
426 #[serde(default, deserialize_with = "de_justify_content")]
427 pub justify_content: Option<JustifyContent>,
428
429 #[serde(default)]
431 pub margin: Option<Rect>,
432 #[serde(default)]
433 pub padding: Option<Rect>,
434 #[serde(default)]
435 pub border: Option<Rect>,
436
437 #[serde(default, deserialize_with = "de_flex_direction")]
439 pub flex_direction: Option<FlexDirection>,
440 #[serde(default, deserialize_with = "de_flex_wrap")]
441 pub flex_wrap: Option<FlexWrap>,
442 #[serde(default)]
443 pub flex_grow: Option<f32>,
444 #[serde(default)]
445 pub flex_shrink: Option<f32>,
446 #[serde(default)]
447 pub flex_basis: Option<Length>,
448 #[serde(default)]
449 pub gap: Option<Length>,
450 #[serde(default)]
451 pub row_gap: Option<Length>,
452 #[serde(default)]
453 pub column_gap: Option<Length>,
454
455 #[serde(default, deserialize_with = "de_grid_auto_flow")]
457 pub grid_auto_flow: Option<GridAutoFlow>,
458 #[serde(default, deserialize_with = "de_grid_template")]
460 pub grid_template_rows: Option<Vec<RepeatedGridTrack>>,
461 #[serde(default, deserialize_with = "de_grid_template")]
462 pub grid_template_columns: Option<Vec<RepeatedGridTrack>>,
463 #[serde(default, deserialize_with = "de_grid_auto_tracks")]
465 pub grid_auto_rows: Option<Vec<GridTrack>>,
466 #[serde(default, deserialize_with = "de_grid_auto_tracks")]
467 pub grid_auto_columns: Option<Vec<GridTrack>>,
468 #[serde(default, deserialize_with = "de_grid_placement")]
470 pub grid_row: Option<GridPlacement>,
471 #[serde(default, deserialize_with = "de_grid_placement")]
472 pub grid_column: Option<GridPlacement>,
473
474 #[serde(default)]
477 pub background_color: Option<String>,
478 #[serde(default)]
481 pub border_color: Option<BorderColorSpec>,
482 #[serde(default)]
485 pub border_radius: Option<Rect>,
486 #[serde(default)]
487 pub outline: Option<OutlineSpec>,
488 #[serde(default)]
489 pub box_shadow: Option<BoxShadowList>,
490 #[serde(default)]
499 pub filter: Option<FilterSpec>,
500 #[serde(default)]
504 pub background_gradient: Option<GradientList>,
505 #[serde(default)]
508 pub border_gradient: Option<GradientList>,
509 #[serde(default)]
510 pub z_index: Option<i32>,
511 #[serde(default)]
515 pub global_z_index: Option<i32>,
516 #[serde(default, deserialize_with = "de_focus_policy")]
522 pub focus_policy: Option<FocusPolicy>,
523 #[serde(default)]
531 pub cursor: Option<String>,
532
533 #[serde(default)]
538 pub transform: Option<Transform>,
539 #[serde(default)]
542 pub opacity: Option<f32>,
543 #[serde(default)]
548 pub transition: Option<crate::transition::Transition>,
549
550 #[serde(default)]
555 pub scrollbar: Option<crate::scrollbar::ScrollbarSpec>,
556
557 #[serde(default)]
560 pub color: Option<String>,
561 #[serde(default)]
564 pub font_size: Option<FontSize>,
565 #[serde(default, deserialize_with = "de_font_weight")]
568 pub font_weight: Option<FontWeight>,
569 #[serde(default)]
573 pub font_family: Option<String>,
574 #[serde(default, deserialize_with = "de_text_align")]
577 pub text_align: Option<Justify>,
578 #[serde(default)]
581 pub line_height: Option<LineHeightSpec>,
582 #[serde(default)]
585 pub letter_spacing: Option<LetterSpacingSpec>,
586 #[serde(default)]
588 pub text_shadow: Option<TextShadowSpec>,
589 #[serde(default, deserialize_with = "de_line_break")]
593 pub line_break: Option<LineBreak>,
594}
595
596pub mod style_groups {
602 pub const LAYOUT: u32 = 1 << 0;
604 pub const BACKGROUND: u32 = 1 << 1;
606 pub const TRANSFORM: u32 = 1 << 2;
608 pub const BORDER_COLOR: u32 = 1 << 3;
610 pub const OUTLINE: u32 = 1 << 4;
612 pub const BOX_SHADOW: u32 = 1 << 5;
614 pub const BG_GRADIENT: u32 = 1 << 6;
616 pub const BORDER_GRADIENT: u32 = 1 << 7;
618 pub const TEXT_SHADOW: u32 = 1 << 8;
620 pub const Z_INDEX: u32 = 1 << 9;
622 pub const GLOBAL_Z_INDEX: u32 = 1 << 10;
624 pub const FOCUS_POLICY: u32 = 1 << 11;
626 pub const FILTER: u32 = 1 << 12;
628 pub const TRANSITION: u32 = 1 << 13;
632 pub const SCROLL_TRANSITION: u32 = 1 << 14;
634 pub const TEXT: u32 = 1 << 15;
638 pub const TEXT_LAYOUT: u32 = 1 << 16;
640 pub const CURSOR: u32 = 1 << 17;
643 pub const SCROLLBAR: u32 = 1 << 18;
648}
649
650macro_rules! with_style_fields {
667 ($cb:ident) => {
668 $cb! {
669 (display, "display", (LAYOUT), overlay),
670 (box_sizing, "boxSizing", (LAYOUT), overlay),
671 (position_type, "positionType", (LAYOUT), overlay),
672 (overflow_x, "overflowX", (LAYOUT), overlay),
673 (overflow_y, "overflowY", (LAYOUT), overlay),
674 (scrollbar_width, "scrollbarWidth", (LAYOUT), overlay),
675 (left, "left", (LAYOUT), overlay),
676 (right, "right", (LAYOUT), overlay),
677 (top, "top", (LAYOUT), overlay),
678 (bottom, "bottom", (LAYOUT), overlay),
679 (width, "width", (LAYOUT | TRANSITION), overlay),
680 (height, "height", (LAYOUT | TRANSITION), overlay),
681 (min_width, "minWidth", (LAYOUT), overlay),
682 (min_height, "minHeight", (LAYOUT), overlay),
683 (max_width, "maxWidth", (LAYOUT | TRANSITION), overlay),
684 (max_height, "maxHeight", (LAYOUT | TRANSITION), overlay),
685 (aspect_ratio, "aspectRatio", (LAYOUT), overlay),
686 (align_items, "alignItems", (LAYOUT), overlay),
687 (justify_items, "justifyItems", (LAYOUT), overlay),
688 (align_self, "alignSelf", (LAYOUT), overlay),
689 (justify_self, "justifySelf", (LAYOUT), overlay),
690 (align_content, "alignContent", (LAYOUT), overlay),
691 (justify_content, "justifyContent", (LAYOUT), overlay),
692 (margin, "margin", (LAYOUT), overlay),
693 (padding, "padding", (LAYOUT), overlay),
694 (border, "border", (LAYOUT), overlay),
695 (flex_direction, "flexDirection", (LAYOUT), overlay),
696 (flex_wrap, "flexWrap", (LAYOUT), overlay),
697 (flex_grow, "flexGrow", (LAYOUT), overlay),
698 (flex_shrink, "flexShrink", (LAYOUT), overlay),
699 (flex_basis, "flexBasis", (LAYOUT), overlay),
700 (gap, "gap", (LAYOUT), overlay),
701 (row_gap, "rowGap", (LAYOUT), overlay),
702 (column_gap, "columnGap", (LAYOUT), overlay),
703 (grid_auto_flow, "gridAutoFlow", (LAYOUT), overlay),
704 (grid_template_rows, "gridTemplateRows", (LAYOUT), overlay),
705 (grid_template_columns, "gridTemplateColumns", (LAYOUT), overlay),
706 (grid_auto_rows, "gridAutoRows", (LAYOUT), overlay),
707 (grid_auto_columns, "gridAutoColumns", (LAYOUT), overlay),
708 (grid_row, "gridRow", (LAYOUT), overlay),
709 (grid_column, "gridColumn", (LAYOUT), overlay),
710 (background_color, "backgroundColor", (BACKGROUND | TRANSITION), overlay),
711 (border_color, "borderColor", (BORDER_COLOR), overlay),
712 (border_radius, "borderRadius", (LAYOUT), overlay),
713 (outline, "outline", (OUTLINE), overlay),
714 (box_shadow, "boxShadow", (BOX_SHADOW), overlay),
715 (filter, "filter", (BACKGROUND | FILTER), no_overlay),
716 (background_gradient, "backgroundGradient", (BG_GRADIENT), overlay),
717 (border_gradient, "borderGradient", (BORDER_GRADIENT), overlay),
718 (z_index, "zIndex", (Z_INDEX), overlay),
719 (global_z_index, "globalZIndex", (GLOBAL_Z_INDEX), overlay),
720 (focus_policy, "focusPolicy", (FOCUS_POLICY), no_overlay),
721 (cursor, "cursor", (CURSOR), overlay),
722 (scrollbar, "scrollbar", (SCROLLBAR | LAYOUT), overlay),
723 (
724 transform,
725 "transform",
726 (TRANSFORM | TRANSITION),
727 overlay
728 ),
729 (
730 opacity,
731 "opacity",
732 (BACKGROUND | BG_GRADIENT | BORDER_GRADIENT | TEXT_SHADOW | TRANSITION | TEXT),
733 overlay
734 ),
735 (
736 transition,
737 "transition",
738 (TRANSITION | SCROLL_TRANSITION),
739 overlay
740 ),
741 (color, "color", (TEXT), overlay),
742 (font_size, "fontSize", (TEXT), overlay),
743 (font_weight, "fontWeight", (TEXT), overlay),
744 (font_family, "fontFamily", (TEXT), overlay),
745 (text_align, "textAlign", (TEXT_LAYOUT), overlay),
746 (line_height, "lineHeight", (TEXT), overlay),
747 (letter_spacing, "letterSpacing", (TEXT), overlay),
748 (text_shadow, "textShadow", (TEXT_SHADOW), overlay),
749 (line_break, "lineBreak", (TEXT_LAYOUT), overlay),
750 }
751 };
752}
753pub(crate) use with_style_fields;
754
755#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
758pub struct StyleDirty(pub u32);
759
760impl StyleDirty {
761 pub const NONE: Self = Self(0);
763 pub const ALL: Self = Self(u32::MAX);
765
766 pub fn intersects(self, groups: u32) -> bool {
768 self.0 & groups != 0
769 }
770
771 pub fn any(self) -> bool {
773 self.0 != 0
774 }
775}
776
777#[derive(Debug, Clone, Copy, Default)]
781pub struct PropsDirty {
782 pub style: StyleDirty,
784 pub hover_style: bool,
786 pub press_style: bool,
788 pub focus_style: bool,
790 pub pointer: bool,
792 pub scroll_listener: bool,
794 pub wheel: bool,
796 pub scroll_step: bool,
798 pub animated: bool,
800 pub anchor: bool,
802 pub image: bool,
805 pub target: bool,
807 pub editable_handlers: bool,
810 pub aria_label: bool,
812}
813
814impl PropsDirty {
815 pub fn any_style_variant(&self) -> bool {
818 self.style.any() || self.hover_style || self.press_style || self.focus_style
819 }
820}
821
822#[derive(Debug, Default)]
827pub struct UpdateEvents {
828 pub value: Option<String>,
830 pub selection_start: Option<usize>,
832 pub selection_end: Option<usize>,
834 pub scroll_top: Option<f32>,
836 pub scroll_left: Option<f32>,
838 pub draw: Option<Vec<DrawCmd>>,
840}
841
842impl Style {
843 pub(crate) fn overlay_delta(&mut self, delta: &Style) -> u32 {
848 let mut groups = 0u32;
849 macro_rules! merge_field {
850 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
851 $(
852 if delta.$f.is_some() {
853 self.$f = delta.$f.clone();
854 groups |= {
855 use style_groups::*;
856 $g
857 };
858 }
859 )*
860 };
861 }
862 with_style_fields!(merge_field);
863 groups
864 }
865
866 pub(crate) fn unset_field(&mut self, wire_name: &str) -> Option<u32> {
869 macro_rules! unset_match {
870 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
871 match wire_name {
872 $(
873 $name => {
874 self.$f = None;
875 Some({
876 use style_groups::*;
877 $g
878 })
879 }
880 )*
881 _ => {
882 tracing::warn!(
883 target: "bevy_react",
884 "unknown style field {wire_name:?} in styleUnset; ignoring"
885 );
886 None
887 }
888 }
889 };
890 }
891 with_style_fields!(unset_match)
892 }
893}
894
895impl Props {
896 pub fn split_events(mut self) -> (Props, UpdateEvents) {
900 let events = UpdateEvents {
901 value: self.value.take(),
902 selection_start: self.selection_start.take(),
903 selection_end: self.selection_end.take(),
904 scroll_top: self.scroll_top.take(),
905 scroll_left: self.scroll_left.take(),
906 draw: self.draw.take(),
907 };
908 (self, events)
909 }
910
911 pub fn merge_delta(
916 &mut self,
917 delta: Props,
918 unset: &[String],
919 style_unset: &[String],
920 ) -> (PropsDirty, UpdateEvents) {
921 let mut dirty = PropsDirty::default();
922 let (delta, events) = delta.split_events();
923
924 if let Some(style_delta) = &delta.style {
926 let groups = self
927 .style
928 .get_or_insert_default()
929 .overlay_delta(style_delta);
930 dirty.style.0 |= groups;
931 }
932 if delta.hover_style.is_some() {
933 self.hover_style = delta.hover_style;
934 dirty.hover_style = true;
935 }
936 if delta.press_style.is_some() {
937 self.press_style = delta.press_style;
938 dirty.press_style = true;
939 }
940 if delta.focus_style.is_some() {
941 self.focus_style = delta.focus_style;
942 dirty.focus_style = true;
943 }
944 macro_rules! merge_bool {
947 ($($f:ident => $flag:ident),* $(,)?) => {
948 $(
949 if delta.$f {
950 self.$f = true;
951 dirty.$flag = true;
952 }
953 )*
954 };
955 }
956 merge_bool!(
957 on_click => pointer,
958 on_pointer_down => pointer,
959 on_pointer_move => pointer,
960 on_pointer_up => pointer,
961 on_pointer_enter => pointer,
962 on_pointer_leave => pointer,
963 on_scroll => scroll_listener,
964 on_wheel => wheel,
965 on_change => editable_handlers,
966 on_select => editable_handlers,
967 on_focus => editable_handlers,
968 on_blur => editable_handlers,
969 flip_x => image,
970 flip_y => image,
971 );
972 if delta.multiline {
975 self.multiline = true;
976 }
977 if delta.autofocus {
978 self.autofocus = true;
979 }
980 if delta.on_resize {
983 self.on_resize = true;
984 }
985 macro_rules! merge_option {
986 ($($f:ident => $($flag:ident)?),* $(,)?) => {
987 $(
988 if delta.$f.is_some() {
989 self.$f = delta.$f;
990 $( dirty.$flag = true; )?
991 }
992 )*
993 };
994 }
995 merge_option!(
996 scroll_step => scroll_step,
997 animated => animated,
998 anchor => anchor,
999 src => image,
1000 tint => image,
1001 image_mode => image,
1002 source_rect => image,
1003 atlas => image,
1004 visual_box => image,
1005 target => target,
1006 aria_label => aria_label,
1007 max_length => , );
1009
1010 for name in unset {
1012 match name.as_str() {
1013 "style" => {
1014 self.style = None;
1015 dirty.style = StyleDirty::ALL;
1016 }
1017 "hoverStyle" => {
1018 self.hover_style = None;
1019 dirty.hover_style = true;
1020 }
1021 "pressStyle" => {
1022 self.press_style = None;
1023 dirty.press_style = true;
1024 }
1025 "focusStyle" => {
1026 self.focus_style = None;
1027 dirty.focus_style = true;
1028 }
1029 "onClick" => {
1030 self.on_click = false;
1031 dirty.pointer = true;
1032 }
1033 "onPointerDown" => {
1034 self.on_pointer_down = false;
1035 dirty.pointer = true;
1036 }
1037 "onPointerMove" => {
1038 self.on_pointer_move = false;
1039 dirty.pointer = true;
1040 }
1041 "onPointerUp" => {
1042 self.on_pointer_up = false;
1043 dirty.pointer = true;
1044 }
1045 "onPointerEnter" => {
1046 self.on_pointer_enter = false;
1047 dirty.pointer = true;
1048 }
1049 "onPointerLeave" => {
1050 self.on_pointer_leave = false;
1051 dirty.pointer = true;
1052 }
1053 "onScroll" => {
1054 self.on_scroll = false;
1055 dirty.scroll_listener = true;
1056 }
1057 "onWheel" => {
1058 self.on_wheel = false;
1059 dirty.wheel = true;
1060 }
1061 "onChange" => {
1062 self.on_change = false;
1063 dirty.editable_handlers = true;
1064 }
1065 "onSelect" => {
1066 self.on_select = false;
1067 dirty.editable_handlers = true;
1068 }
1069 "onFocus" => {
1070 self.on_focus = false;
1071 dirty.editable_handlers = true;
1072 }
1073 "onBlur" => {
1074 self.on_blur = false;
1075 dirty.editable_handlers = true;
1076 }
1077 "flipX" => {
1078 self.flip_x = false;
1079 dirty.image = true;
1080 }
1081 "flipY" => {
1082 self.flip_y = false;
1083 dirty.image = true;
1084 }
1085 "multiline" => self.multiline = false,
1086 "autofocus" => self.autofocus = false,
1087 "onResize" => self.on_resize = false,
1088 "scrollStep" => {
1089 self.scroll_step = None;
1090 dirty.scroll_step = true;
1091 }
1092 "animated" => {
1093 self.animated = None;
1094 dirty.animated = true;
1095 }
1096 "anchor" => {
1097 self.anchor = None;
1098 dirty.anchor = true;
1099 }
1100 "src" => {
1101 self.src = None;
1102 dirty.image = true;
1103 }
1104 "tint" => {
1105 self.tint = None;
1106 dirty.image = true;
1107 }
1108 "imageMode" => {
1109 self.image_mode = None;
1110 dirty.image = true;
1111 }
1112 "sourceRect" => {
1113 self.source_rect = None;
1114 dirty.image = true;
1115 }
1116 "atlas" => {
1117 self.atlas = None;
1118 dirty.image = true;
1119 }
1120 "visualBox" => {
1121 self.visual_box = None;
1122 dirty.image = true;
1123 }
1124 "target" => {
1125 self.target = None;
1126 dirty.target = true;
1127 }
1128 "ariaLabel" => {
1129 self.aria_label = None;
1130 dirty.aria_label = true;
1131 }
1132 "maxLength" => self.max_length = None,
1133 "value" | "selectionStart" | "selectionEnd" | "scrollTop" | "scrollLeft"
1136 | "draw" => {
1137 tracing::warn!(
1138 target: "bevy_react",
1139 "event-like prop {name:?} in unset; nothing to reset"
1140 );
1141 }
1142 other => {
1143 tracing::warn!(
1144 target: "bevy_react",
1145 "unknown prop {other:?} in unset; ignoring"
1146 );
1147 }
1148 }
1149 }
1150
1151 if !style_unset.is_empty() {
1154 let style = self.style.get_or_insert_default();
1155 for name in style_unset {
1156 if let Some(groups) = style.unset_field(name) {
1157 dirty.style.0 |= groups;
1158 }
1159 }
1160 }
1161
1162 (dirty, events)
1163 }
1164}
1165
1166#[derive(Debug, Clone, Default, Deserialize)]
1168#[serde(rename_all = "camelCase")]
1169pub struct OutlineSpec {
1170 #[serde(default)]
1171 pub width: Option<Length>,
1172 #[serde(default)]
1173 pub offset: Option<Length>,
1174 #[serde(default)]
1175 pub color: Option<String>,
1176}
1177
1178#[derive(Debug, Clone, Default, Deserialize)]
1180#[serde(rename_all = "camelCase")]
1181pub struct BoxShadowSpec {
1182 #[serde(default)]
1183 pub color: Option<String>,
1184 #[serde(default)]
1185 pub x_offset: Option<Length>,
1186 #[serde(default)]
1187 pub y_offset: Option<Length>,
1188 #[serde(default)]
1189 pub spread_radius: Option<Length>,
1190 #[serde(default)]
1191 pub blur_radius: Option<Length>,
1192}
1193
1194#[derive(Debug, Clone, Deserialize)]
1196#[serde(untagged)]
1197pub enum BoxShadowList {
1198 One(BoxShadowSpec),
1199 Many(Vec<BoxShadowSpec>),
1200}
1201
1202#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
1211#[serde(rename_all = "camelCase")]
1212pub struct FilterSpec {
1213 #[serde(default)]
1215 pub blur: Option<Length>,
1216 #[serde(default)]
1218 pub brightness: Option<f32>,
1219 #[serde(default)]
1221 pub contrast: Option<f32>,
1222 #[serde(default)]
1224 pub saturate: Option<f32>,
1225 #[serde(default)]
1227 pub grayscale: Option<f32>,
1228 #[serde(default)]
1230 pub sepia: Option<f32>,
1231 #[serde(default)]
1233 pub invert: Option<f32>,
1234 #[serde(default)]
1236 pub hue_rotate: Option<Angle>,
1237}
1238
1239#[derive(Debug, Clone, Deserialize)]
1243#[serde(untagged)]
1244pub enum LineHeightSpec {
1245 Relative(f32),
1246 Px { px: f32 },
1247 Str(String),
1248}
1249
1250#[derive(Debug, Clone, Deserialize)]
1254#[serde(untagged)]
1255pub enum LetterSpacingSpec {
1256 Px(f32),
1257 Rem { rem: f32 },
1258 Str(String),
1259}
1260
1261#[derive(Debug, Clone, Default, Deserialize)]
1265#[serde(rename_all = "camelCase")]
1266pub struct TextShadowSpec {
1267 #[serde(default)]
1268 pub color: Option<String>,
1269 #[serde(default)]
1270 pub offset_x: Option<f32>,
1271 #[serde(default)]
1272 pub offset_y: Option<f32>,
1273}
1274
1275#[derive(Debug, Clone, Deserialize)]
1280#[serde(rename_all = "camelCase")]
1281pub struct GradientStop {
1282 pub color: String,
1283 #[serde(default)]
1284 pub position: Option<Length>,
1285 #[serde(default)]
1286 pub hint: Option<f32>,
1287}
1288
1289#[derive(Debug, Clone, Deserialize)]
1292#[serde(rename_all = "camelCase")]
1293pub struct AngularStop {
1294 pub color: String,
1295 #[serde(default)]
1296 pub angle: Option<Angle>,
1297 #[serde(default)]
1298 pub hint: Option<f32>,
1299}
1300
1301pub type GradientPosition = String;
1304
1305pub type ColorSpace = String;
1309
1310#[derive(Debug, Clone, Deserialize)]
1314#[serde(rename_all = "camelCase")]
1315pub enum RadialShapeSpec {
1316 Keyword(String),
1317 Circle { circle: Length },
1318 Ellipse { ellipse: [Length; 2] },
1319}
1320
1321#[derive(Debug, Clone, Default, Deserialize)]
1322#[serde(rename_all = "camelCase")]
1323pub struct LinearGradientSpec {
1324 #[serde(default)]
1327 pub angle: Option<Angle>,
1328 #[serde(default)]
1329 pub stops: Vec<GradientStop>,
1330 #[serde(default)]
1331 pub color_space: Option<ColorSpace>,
1332}
1333
1334#[derive(Debug, Clone, Default, Deserialize)]
1335#[serde(rename_all = "camelCase")]
1336pub struct RadialGradientSpec {
1337 #[serde(default)]
1338 pub position: Option<GradientPosition>,
1339 #[serde(default)]
1340 pub shape: Option<RadialShapeSpec>,
1341 #[serde(default)]
1342 pub stops: Vec<GradientStop>,
1343 #[serde(default)]
1344 pub color_space: Option<ColorSpace>,
1345}
1346
1347#[derive(Debug, Clone, Default, Deserialize)]
1348#[serde(rename_all = "camelCase")]
1349pub struct ConicGradientSpec {
1350 #[serde(default)]
1352 pub start: Option<Angle>,
1353 #[serde(default)]
1354 pub position: Option<GradientPosition>,
1355 #[serde(default)]
1356 pub stops: Vec<AngularStop>,
1357 #[serde(default)]
1358 pub color_space: Option<ColorSpace>,
1359}
1360
1361#[derive(Debug, Clone, Deserialize)]
1363#[serde(tag = "type", rename_all = "camelCase")]
1364pub enum GradientSpec {
1365 Linear(LinearGradientSpec),
1366 Radial(RadialGradientSpec),
1367 Conic(ConicGradientSpec),
1368}
1369
1370#[derive(Debug, Clone, Deserialize)]
1372#[serde(untagged)]
1373pub enum GradientList {
1374 One(GradientSpec),
1375 Many(Vec<GradientSpec>),
1376}
1377
1378#[derive(Debug, Clone, Deserialize)]
1383#[serde(untagged)]
1384pub enum ImageMode {
1385 Keyword(String),
1387 Spec(ImageModeSpec),
1388}
1389
1390#[derive(Debug, Clone, Deserialize)]
1392#[serde(tag = "type", rename_all = "camelCase")]
1393pub enum ImageModeSpec {
1394 Sliced(SliceSpec),
1395 Tiled(TiledSpec),
1396}
1397
1398#[derive(Debug, Clone, Default, Deserialize)]
1400#[serde(rename_all = "camelCase")]
1401pub struct SliceSpec {
1402 #[serde(default)]
1405 pub border: SliceBorder,
1406 #[serde(default)]
1408 pub center_scale_mode: Option<SliceScale>,
1409 #[serde(default)]
1411 pub sides_scale_mode: Option<SliceScale>,
1412 #[serde(default)]
1414 pub max_corner_scale: Option<f32>,
1415}
1416
1417#[derive(Debug, Clone, Default, Deserialize)]
1420#[serde(untagged)]
1421pub enum SliceBorder {
1422 #[default]
1424 Zero,
1425 Uniform(f32),
1427 Sides {
1429 #[serde(default)]
1430 top: f32,
1431 #[serde(default)]
1432 right: f32,
1433 #[serde(default)]
1434 bottom: f32,
1435 #[serde(default)]
1436 left: f32,
1437 },
1438}
1439
1440#[derive(Debug, Clone, Deserialize)]
1443#[serde(untagged)]
1444pub enum SliceScale {
1445 Keyword(String),
1446 Tile { tile: f32 },
1447}
1448
1449#[derive(Debug, Clone, Default, Deserialize)]
1451#[serde(rename_all = "camelCase")]
1452pub struct TiledSpec {
1453 #[serde(default)]
1454 pub tile_x: bool,
1455 #[serde(default)]
1456 pub tile_y: bool,
1457 #[serde(default)]
1459 pub stretch_value: Option<f32>,
1460}
1461
1462#[derive(Debug, Clone, Copy, Deserialize)]
1465#[serde(rename_all = "camelCase")]
1466pub struct SourceRect {
1467 pub x: f32,
1468 pub y: f32,
1469 pub width: f32,
1470 pub height: f32,
1471}
1472
1473#[derive(Debug, Clone, Deserialize)]
1478#[serde(rename_all = "camelCase")]
1479pub struct AtlasSpec {
1480 pub tile_width: u32,
1481 pub tile_height: u32,
1482 pub columns: u32,
1483 pub rows: u32,
1484 #[serde(default)]
1486 pub padding: Option<[u32; 2]>,
1487 #[serde(default)]
1489 pub offset: Option<[u32; 2]>,
1490 #[serde(default)]
1492 pub index: usize,
1493}
1494
1495#[derive(Debug, Clone, Copy, Default, PartialEq, Deserialize)]
1499#[serde(rename_all = "camelCase")]
1500pub struct Transform {
1501 pub translate_x: Option<Length>,
1504 pub translate_y: Option<Length>,
1507 pub scale: Option<f32>,
1509 pub scale_x: Option<f32>,
1510 pub scale_y: Option<f32>,
1511 pub rotate: Option<Angle>,
1513}
1514
1515#[derive(Debug, Clone, Copy, PartialEq)]
1518pub enum Length {
1519 Auto,
1520 Px(f32),
1521 Percent(f32),
1522 Vw(f32),
1523 Vh(f32),
1524 VMin(f32),
1525 VMax(f32),
1526}
1527
1528impl Default for Length {
1529 fn default() -> Self {
1530 Length::Px(0.0)
1531 }
1532}
1533
1534fn parse_length(s: &str) -> Result<Length, String> {
1536 let s = s.trim();
1537 if s.eq_ignore_ascii_case("auto") {
1538 return Ok(Length::Auto);
1539 }
1540 type LengthCtor = fn(f32) -> Length;
1543 let units: [(&str, LengthCtor); 6] = [
1544 ("px", Length::Px),
1545 ("vmin", Length::VMin),
1546 ("vmax", Length::VMax),
1547 ("vw", Length::Vw),
1548 ("vh", Length::Vh),
1549 ("%", Length::Percent),
1550 ];
1551 for (suffix, ctor) in units {
1552 if let Some(num) = s.strip_suffix(suffix) {
1553 let v: f32 = num
1554 .trim()
1555 .parse()
1556 .map_err(|_| format!("invalid length {s:?}"))?;
1557 return Ok(ctor(v));
1558 }
1559 }
1560 s.parse::<f32>()
1561 .map(Length::Px)
1562 .map_err(|_| format!("invalid length {s:?}"))
1563}
1564
1565impl<'de> Deserialize<'de> for Length {
1566 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1567 struct LengthVisitor;
1568 impl<'de> Visitor<'de> for LengthVisitor {
1569 type Value = Length;
1570 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1571 f.write_str("a number (logical pixels) or a CSS length string")
1572 }
1573 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Length, E> {
1574 Ok(Length::Px(v as f32))
1575 }
1576 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Length, E> {
1577 Ok(Length::Px(v as f32))
1578 }
1579 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Length, E> {
1580 Ok(Length::Px(v as f32))
1581 }
1582 fn visit_str<E: de::Error>(self, s: &str) -> Result<Length, E> {
1583 Ok(parse_length(s).unwrap_or_else(|e| {
1584 decode_warn("length", s, &e);
1585 Length::default()
1586 }))
1587 }
1588 }
1589 d.deserialize_any(LengthVisitor)
1590 }
1591}
1592
1593#[derive(Debug, Clone, Copy, PartialEq, Default)]
1597pub struct Angle(f32);
1598
1599impl Angle {
1600 pub fn radians(self) -> f32 {
1602 self.0
1603 }
1604}
1605
1606fn parse_angle(s: &str) -> Result<f32, String> {
1610 use std::f32::consts::{PI, TAU};
1611 let s = s.trim();
1612 type AngleConv = fn(f32) -> f32;
1613 let units: [(&str, AngleConv); 4] = [
1614 ("deg", f32::to_radians),
1615 ("grad", |v| v * PI / 200.0),
1616 ("turn", |v| v * TAU),
1617 ("rad", |v| v),
1618 ];
1619 for (suffix, conv) in units {
1620 if let Some(num) = s.strip_suffix(suffix) {
1621 let v: f32 = num
1622 .trim()
1623 .parse()
1624 .map_err(|_| format!("invalid angle {s:?}"))?;
1625 return Ok(conv(v));
1626 }
1627 }
1628 s.parse::<f32>()
1629 .map(f32::to_radians)
1630 .map_err(|_| format!("invalid angle {s:?}"))
1631}
1632
1633impl<'de> Deserialize<'de> for Angle {
1634 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1635 struct AngleVisitor;
1636 impl Visitor<'_> for AngleVisitor {
1637 type Value = Angle;
1638 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1639 f.write_str("a number (degrees) or a CSS angle string")
1640 }
1641 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Angle, E> {
1642 Ok(Angle((v as f32).to_radians()))
1643 }
1644 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Angle, E> {
1645 Ok(Angle((v as f32).to_radians()))
1646 }
1647 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Angle, E> {
1648 Ok(Angle((v as f32).to_radians()))
1649 }
1650 fn visit_str<E: de::Error>(self, s: &str) -> Result<Angle, E> {
1651 Ok(parse_angle(s).map(Angle).unwrap_or_else(|e| {
1652 decode_warn("angle", s, &e);
1653 Angle::default()
1654 }))
1655 }
1656 }
1657 d.deserialize_any(AngleVisitor)
1658 }
1659}
1660
1661#[derive(Debug, Clone, Copy, PartialEq, Default)]
1665pub struct Time(f32);
1666
1667impl Time {
1668 pub fn from_secs(secs: f32) -> Self {
1670 Time(secs)
1671 }
1672 pub fn seconds(self) -> f32 {
1674 self.0
1675 }
1676}
1677
1678fn parse_time(s: &str) -> Result<f32, String> {
1682 let s = s.trim();
1683 if let Some(num) = s.strip_suffix("ms") {
1684 return num
1685 .trim()
1686 .parse::<f32>()
1687 .map(|v| v / 1000.0)
1688 .map_err(|_| format!("invalid time {s:?}"));
1689 }
1690 if let Some(num) = s.strip_suffix('s') {
1691 return num
1692 .trim()
1693 .parse::<f32>()
1694 .map_err(|_| format!("invalid time {s:?}"));
1695 }
1696 s.parse::<f32>()
1697 .map(|v| v / 1000.0)
1698 .map_err(|_| format!("invalid time {s:?}"))
1699}
1700
1701impl<'de> Deserialize<'de> for Time {
1702 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1703 struct TimeVisitor;
1704 impl Visitor<'_> for TimeVisitor {
1705 type Value = Time;
1706 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1707 f.write_str("a number (milliseconds) or a CSS time string")
1708 }
1709 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Time, E> {
1710 Ok(Time(v as f32 / 1000.0))
1711 }
1712 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Time, E> {
1713 Ok(Time(v as f32 / 1000.0))
1714 }
1715 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Time, E> {
1716 Ok(Time(v as f32 / 1000.0))
1717 }
1718 fn visit_str<E: de::Error>(self, s: &str) -> Result<Time, E> {
1719 Ok(parse_time(s).map(Time).unwrap_or_else(|e| {
1720 decode_warn("time", s, &e);
1721 Time::default()
1722 }))
1723 }
1724 }
1725 d.deserialize_any(TimeVisitor)
1726 }
1727}
1728
1729#[derive(Debug, Clone, Copy, PartialEq)]
1734pub enum FontSize {
1735 Px(f32),
1736 Vw(f32),
1737 Vh(f32),
1738 VMin(f32),
1739 VMax(f32),
1740 Rem(f32),
1741}
1742
1743fn parse_font_size(s: &str) -> Result<FontSize, String> {
1747 let s = s.trim();
1748 type FsCtor = fn(f32) -> FontSize;
1749 let units: [(&str, FsCtor); 6] = [
1750 ("px", FontSize::Px),
1751 ("rem", FontSize::Rem),
1752 ("vmin", FontSize::VMin),
1753 ("vmax", FontSize::VMax),
1754 ("vw", FontSize::Vw),
1755 ("vh", FontSize::Vh),
1756 ];
1757 for (suffix, ctor) in units {
1758 if let Some(num) = s.strip_suffix(suffix) {
1759 let v: f32 = num
1760 .trim()
1761 .parse()
1762 .map_err(|_| format!("invalid fontSize {s:?}"))?;
1763 return Ok(ctor(v));
1764 }
1765 }
1766 s.parse::<f32>()
1767 .map(FontSize::Px)
1768 .map_err(|_| format!("invalid fontSize {s:?}"))
1769}
1770
1771impl<'de> Deserialize<'de> for FontSize {
1772 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1773 struct FontSizeVisitor;
1774 impl Visitor<'_> for FontSizeVisitor {
1775 type Value = FontSize;
1776 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1777 f.write_str("a number (logical pixels) or a font-size unit string")
1778 }
1779 fn visit_f64<E: de::Error>(self, v: f64) -> Result<FontSize, E> {
1780 Ok(FontSize::Px(v as f32))
1781 }
1782 fn visit_i64<E: de::Error>(self, v: i64) -> Result<FontSize, E> {
1783 Ok(FontSize::Px(v as f32))
1784 }
1785 fn visit_u64<E: de::Error>(self, v: u64) -> Result<FontSize, E> {
1786 Ok(FontSize::Px(v as f32))
1787 }
1788 fn visit_str<E: de::Error>(self, s: &str) -> Result<FontSize, E> {
1789 Ok(parse_font_size(s).unwrap_or_else(|e| {
1790 decode_warn("fontSize", s, &e);
1791 FontSize::Px(0.0)
1792 }))
1793 }
1794 }
1795 d.deserialize_any(FontSizeVisitor)
1796 }
1797}
1798
1799#[derive(Debug, Clone, Copy, PartialEq, Default)]
1802pub struct Rect {
1803 pub top: Length,
1804 pub right: Length,
1805 pub bottom: Length,
1806 pub left: Length,
1807}
1808
1809impl Rect {
1810 fn uniform(v: Length) -> Self {
1811 Rect {
1812 top: v,
1813 right: v,
1814 bottom: v,
1815 left: v,
1816 }
1817 }
1818
1819 fn from_shorthand(values: &[Length]) -> Result<Self, String> {
1821 Ok(match values {
1822 [a] => Rect::uniform(*a),
1823 [a, b] => Rect {
1824 top: *a,
1825 bottom: *a,
1826 right: *b,
1827 left: *b,
1828 },
1829 [a, b, c] => Rect {
1830 top: *a,
1831 right: *b,
1832 left: *b,
1833 bottom: *c,
1834 },
1835 [a, b, c, d] => Rect {
1836 top: *a,
1837 right: *b,
1838 bottom: *c,
1839 left: *d,
1840 },
1841 _ => return Err("expected 1–4 length values".into()),
1842 })
1843 }
1844}
1845
1846impl<'de> Deserialize<'de> for Rect {
1847 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1848 struct RectVisitor;
1849 impl<'de> Visitor<'de> for RectVisitor {
1850 type Value = Rect;
1851 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1852 f.write_str("a number, a CSS shorthand string, or a {top,right,bottom,left} object")
1853 }
1854 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Rect, E> {
1855 Ok(Rect::uniform(Length::Px(v as f32)))
1856 }
1857 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Rect, E> {
1858 Ok(Rect::uniform(Length::Px(v as f32)))
1859 }
1860 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Rect, E> {
1861 Ok(Rect::uniform(Length::Px(v as f32)))
1862 }
1863 fn visit_str<E: de::Error>(self, s: &str) -> Result<Rect, E> {
1864 let values: Vec<Length> = s
1867 .split_whitespace()
1868 .map(|tok| {
1869 parse_length(tok).unwrap_or_else(|e| {
1870 decode_warn("rect", tok, &e);
1871 Length::default()
1872 })
1873 })
1874 .collect();
1875 Ok(Rect::from_shorthand(&values).unwrap_or_else(|e| {
1876 decode_warn("rect", s, &format!("invalid rect {s:?}: {e}"));
1877 Rect::default()
1878 }))
1879 }
1880 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Rect, A::Error> {
1881 let mut rect = Rect::default();
1882 while let Some(key) = map.next_key::<String>()? {
1883 let v = map.next_value::<Length>()?;
1884 match key.as_str() {
1885 "top" => rect.top = v,
1886 "right" => rect.right = v,
1887 "bottom" => rect.bottom = v,
1888 "left" => rect.left = v,
1889 _ => decode_warn(
1892 "rect",
1893 &key,
1894 &format!(
1895 "unknown rect side {key:?}; ignoring (expected top/right/bottom/left)"
1896 ),
1897 ),
1898 }
1899 }
1900 Ok(rect)
1901 }
1902 }
1903 d.deserialize_any(RectVisitor)
1904 }
1905}
1906
1907macro_rules! keyword_fields {
1914 ( $(
1915 $(#[$meta:meta])*
1916 fn $fn_name:ident($kind:literal) -> $ty:ty {
1917 $( $($kw:literal)|+ => $variant:ident ),+ $(,)?
1918 }
1919 )+ ) => { $(
1920 $(#[$meta])*
1921 fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
1922 struct V;
1923 impl<'de> Visitor<'de> for V {
1924 type Value = Option<$ty>;
1925 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1926 f.write_str(concat!("a `", $kind, "` keyword string"))
1927 }
1928 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
1929 Ok(Some(match s {
1930 $( $($kw)|+ => <$ty>::$variant, )+
1931 _ => {
1932 decode_warn(
1933 $kind,
1934 s,
1935 &format!("unrecognized {} {s:?}", $kind),
1936 );
1937 <$ty>::default()
1938 }
1939 }))
1940 }
1941 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
1942 Ok(None)
1943 }
1944 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
1945 Ok(None)
1946 }
1947 }
1948 d.deserialize_any(V)
1949 }
1950 )+ };
1951}
1952
1953keyword_fields! {
1954 fn de_display("display") -> Display {
1955 "flex" => Flex, "grid" => Grid, "block" => Block, "none" => None,
1956 }
1957 fn de_box_sizing("boxSizing") -> BoxSizing {
1958 "borderBox" | "border-box" => BorderBox,
1959 "contentBox" | "content-box" => ContentBox,
1960 }
1961 fn de_position_type("positionType") -> PositionType {
1962 "absolute" => Absolute, "relative" => Relative,
1963 }
1964 fn de_overflow_axis("overflow") -> OverflowAxis {
1965 "visible" => Visible, "clip" => Clip, "hidden" => Hidden, "scroll" => Scroll,
1966 }
1967 fn de_align_items("alignItems") -> AlignItems {
1973 "start" => Start, "end" => End,
1974 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1975 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1976 }
1977 fn de_justify_items("justifyItems") -> JustifyItems {
1978 "start" => Start, "end" => End,
1979 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1980 }
1981 fn de_align_self("alignSelf") -> AlignSelf {
1982 "auto" => Auto, "start" => Start, "end" => End,
1983 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1984 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1985 }
1986 fn de_justify_self("justifySelf") -> JustifySelf {
1987 "auto" => Auto, "start" => Start, "end" => End,
1988 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
1989 }
1990 fn de_align_content("alignContent") -> AlignContent {
1991 "start" => Start, "end" => End,
1992 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
1993 "center" => Center, "stretch" => Stretch,
1994 "spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
1995 "spaceAround" => SpaceAround,
1996 }
1997 fn de_justify_content("justifyContent") -> JustifyContent {
1998 "start" => Start, "end" => End,
1999 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
2000 "center" => Center, "stretch" => Stretch,
2001 "spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
2002 "spaceAround" => SpaceAround,
2003 }
2004 fn de_flex_direction("flexDirection") -> FlexDirection {
2005 "row" => Row, "column" => Column,
2006 "rowReverse" => RowReverse, "columnReverse" => ColumnReverse,
2007 }
2008 fn de_flex_wrap("flexWrap") -> FlexWrap {
2009 "nowrap" | "noWrap" => NoWrap, "wrap" => Wrap, "wrapReverse" => WrapReverse,
2010 }
2011 fn de_grid_auto_flow("gridAutoFlow") -> GridAutoFlow {
2012 "row" => Row, "column" => Column,
2013 "rowDense" => RowDense, "columnDense" => ColumnDense,
2014 }
2015 fn de_focus_policy("focusPolicy") -> FocusPolicy {
2018 "block" => Block, "pass" => Pass,
2019 }
2020 fn de_text_align("textAlign") -> Justify {
2021 "left" => Left, "center" => Center, "right" => Right,
2022 "justify" => Justified, "start" => Start, "end" => End,
2023 }
2024 fn de_line_break("lineBreak") -> LineBreak {
2025 "wordBoundary" => WordBoundary, "anyCharacter" => AnyCharacter,
2026 "wordOrCharacter" => WordOrCharacter, "noWrap" => NoWrap,
2027 }
2028}
2029
2030fn de_font_weight<'de, D: Deserializer<'de>>(d: D) -> Result<Option<FontWeight>, D::Error> {
2034 struct V;
2035 impl<'de> Visitor<'de> for V {
2036 type Value = Option<FontWeight>;
2037 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2038 f.write_str("a `fontWeight` keyword or numeric weight string")
2039 }
2040 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2041 Ok(Some(match s {
2042 "thin" => FontWeight::THIN,
2043 "light" => FontWeight(300),
2044 "normal" => FontWeight::NORMAL,
2045 "medium" => FontWeight(500),
2046 "semibold" => FontWeight(600),
2047 "bold" => FontWeight::BOLD,
2048 "black" => FontWeight::BLACK,
2049 other => other.parse::<u16>().map(FontWeight).unwrap_or_else(|_| {
2050 decode_warn(
2051 "fontWeight",
2052 other,
2053 &format!("unrecognized fontWeight {other:?}"),
2054 );
2055 FontWeight::NORMAL
2056 }),
2057 }))
2058 }
2059 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
2060 Ok(None)
2061 }
2062 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
2063 Ok(None)
2064 }
2065 }
2066 d.deserialize_any(V)
2067}
2068
2069fn split_tracks(s: &str) -> Vec<String> {
2072 let mut out = Vec::new();
2073 let mut depth = 0usize;
2074 let mut cur = String::new();
2075 for ch in s.chars() {
2076 match ch {
2077 '(' => {
2078 depth += 1;
2079 cur.push(ch);
2080 }
2081 ')' => {
2082 depth = depth.saturating_sub(1);
2083 cur.push(ch);
2084 }
2085 c if c.is_whitespace() && depth == 0 => {
2086 if !cur.is_empty() {
2087 out.push(std::mem::take(&mut cur));
2088 }
2089 }
2090 c => cur.push(c),
2091 }
2092 }
2093 if !cur.is_empty() {
2094 out.push(cur);
2095 }
2096 out
2097}
2098
2099fn single_track(token: &str) -> Option<GridTrack> {
2102 let t = token.trim();
2103 match t {
2104 "auto" => return Some(GridTrack::auto()),
2105 "min-content" => return Some(GridTrack::min_content()),
2106 "max-content" => return Some(GridTrack::max_content()),
2107 _ => {}
2108 }
2109 let parse = |num: &str| num.trim().parse::<f32>().ok();
2110 if let Some(v) = t.strip_suffix("fr").and_then(parse) {
2111 Some(GridTrack::fr(v))
2112 } else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
2113 Some(GridTrack::flex(v))
2114 } else if let Some(v) = t.strip_suffix("px").and_then(parse) {
2115 Some(GridTrack::px(v))
2116 } else {
2117 t.strip_suffix('%').and_then(parse).map(GridTrack::percent)
2118 }
2119}
2120
2121fn repeated_track(count: u16, token: &str) -> Option<RepeatedGridTrack> {
2123 let t = token.trim();
2124 match t {
2125 "auto" => return Some(RepeatedGridTrack::auto(count)),
2126 "min-content" => return Some(RepeatedGridTrack::min_content(count)),
2127 "max-content" => return Some(RepeatedGridTrack::max_content(count)),
2128 _ => {}
2129 }
2130 let parse = |num: &str| num.trim().parse::<f32>().ok();
2131 if let Some(v) = t.strip_suffix("fr").and_then(parse) {
2132 Some(RepeatedGridTrack::fr(count, v))
2133 } else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
2134 Some(RepeatedGridTrack::flex(count, v))
2135 } else if let Some(v) = t.strip_suffix("px").and_then(parse) {
2136 Some(RepeatedGridTrack::px(count as usize, v))
2137 } else {
2138 t.strip_suffix('%')
2139 .and_then(parse)
2140 .map(|v| RepeatedGridTrack::percent(count as usize, v))
2141 }
2142}
2143
2144fn parse_template(s: &str) -> Vec<RepeatedGridTrack> {
2147 split_tracks(s)
2148 .into_iter()
2149 .filter_map(|tok| {
2150 let parse_one = || {
2151 if let Some(inner) = tok
2152 .strip_prefix("repeat(")
2153 .and_then(|t| t.strip_suffix(')'))
2154 {
2155 let (count, track) = inner.split_once(',')?;
2156 repeated_track(count.trim().parse().ok()?, track)
2157 } else {
2158 single_track(&tok).map(Into::into)
2159 }
2160 };
2161 let parsed = parse_one();
2162 if parsed.is_none() {
2163 decode_warn(
2164 "gridTrack",
2165 &tok,
2166 &format!("ignoring unparsable grid track {tok:?}"),
2167 );
2168 }
2169 parsed
2170 })
2171 .collect()
2172}
2173
2174fn parse_auto_tracks(s: &str) -> Vec<GridTrack> {
2176 split_tracks(s)
2177 .iter()
2178 .filter_map(|t| {
2179 let parsed = single_track(t);
2180 if parsed.is_none() {
2181 decode_warn(
2182 "gridTrack",
2183 t,
2184 &format!("ignoring unparsable grid track {t:?}"),
2185 );
2186 }
2187 parsed
2188 })
2189 .collect()
2190}
2191
2192fn try_grid_placement(s: &str) -> Option<GridPlacement> {
2197 enum Token {
2198 Num(i16), Span(u16), Auto,
2201 Invalid, }
2203 fn token(t: &str) -> Token {
2204 let t = t.trim();
2205 if t == "auto" {
2206 return Token::Auto;
2207 }
2208 if let Some(n) = t.strip_prefix("span") {
2209 return match n.trim().parse::<u16>() {
2210 Ok(0) | Err(_) => Token::Invalid,
2211 Ok(n) => Token::Span(n),
2212 };
2213 }
2214 match t.parse::<i16>() {
2215 Ok(0) | Err(_) => Token::Invalid,
2216 Ok(n) => Token::Num(n),
2217 }
2218 }
2219 use Token::*;
2220 if let Some((a, b)) = s.split_once('/') {
2221 return Some(match (token(a), token(b)) {
2222 (Num(start), Span(span)) => GridPlacement::start_span(start, span),
2223 (Auto, Span(span)) => GridPlacement::span(span),
2224 (Num(start), Num(end)) => GridPlacement::start_end(start, end),
2225 (Num(start), Auto) => GridPlacement::start(start),
2226 (Auto, Num(end)) => GridPlacement::end(end),
2227 (Auto, Auto) => GridPlacement::auto(),
2228 _ => return None,
2229 });
2230 }
2231 match token(s) {
2232 Auto => Some(GridPlacement::auto()),
2233 Span(span) => Some(GridPlacement::span(span)),
2234 Num(line) => Some(GridPlacement::start(line)),
2235 Invalid => None,
2236 }
2237}
2238
2239macro_rules! grid_fields {
2242 ( $(
2243 $(#[$meta:meta])*
2244 fn $fn_name:ident($expect:literal) -> $ty:ty { $parse:expr }
2245 )+ ) => { $(
2246 $(#[$meta])*
2247 fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
2248 struct V;
2249 impl<'de> Visitor<'de> for V {
2250 type Value = Option<$ty>;
2251 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2252 f.write_str($expect)
2253 }
2254 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2255 let parse: fn(&str) -> $ty = $parse;
2256 Ok(Some(parse(s)))
2257 }
2258 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
2259 Ok(None)
2260 }
2261 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
2262 Ok(None)
2263 }
2264 }
2265 d.deserialize_any(V)
2266 }
2267 )+ };
2268}
2269
2270grid_fields! {
2271 fn de_grid_template("a CSS grid template string") -> Vec<RepeatedGridTrack> {
2272 parse_template
2273 }
2274 fn de_grid_auto_tracks("a grid auto-track list string") -> Vec<GridTrack> {
2275 parse_auto_tracks
2276 }
2277 fn de_grid_placement("a grid line placement string") -> GridPlacement {
2281 |s| {
2282 try_grid_placement(s).unwrap_or_else(|| {
2283 decode_warn(
2284 "gridPlacement",
2285 s,
2286 &format!("unrecognized grid placement {s:?}"),
2287 );
2288 GridPlacement::default()
2289 })
2290 }
2291 }
2292}
2293
2294#[derive(Debug, Clone, PartialEq, Default)]
2302pub struct BorderColorSpec {
2303 pub top: Option<String>,
2304 pub right: Option<String>,
2305 pub bottom: Option<String>,
2306 pub left: Option<String>,
2307}
2308
2309impl BorderColorSpec {
2310 fn uniform(s: String) -> Self {
2312 BorderColorSpec {
2313 top: Some(s.clone()),
2314 right: Some(s.clone()),
2315 bottom: Some(s.clone()),
2316 left: Some(s),
2317 }
2318 }
2319}
2320
2321impl<'de> Deserialize<'de> for BorderColorSpec {
2322 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2323 struct BorderColorVisitor;
2324 impl<'de> Visitor<'de> for BorderColorVisitor {
2325 type Value = BorderColorSpec;
2326 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2327 f.write_str("a CSS color string or a {top,right,bottom,left} object of colors")
2328 }
2329 fn visit_str<E: de::Error>(self, s: &str) -> Result<BorderColorSpec, E> {
2330 Ok(BorderColorSpec::uniform(s.to_owned()))
2331 }
2332 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<BorderColorSpec, A::Error> {
2333 let mut spec = BorderColorSpec::default();
2334 while let Some(key) = map.next_key::<String>()? {
2335 let v = map.next_value::<String>()?;
2336 match key.as_str() {
2337 "top" => spec.top = Some(v),
2338 "right" => spec.right = Some(v),
2339 "bottom" => spec.bottom = Some(v),
2340 "left" => spec.left = Some(v),
2341 _ => decode_warn(
2344 "borderColor",
2345 &key,
2346 &format!(
2347 "unknown borderColor side {key:?}; ignoring (expected top/right/bottom/left)"
2348 ),
2349 ),
2350 }
2351 }
2352 Ok(spec)
2353 }
2354 }
2355 d.deserialize_any(BorderColorVisitor)
2356 }
2357}
2358
2359#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2362#[serde(rename_all = "camelCase")]
2363pub struct UiEvent {
2364 pub id: NodeId,
2365 pub kind: String,
2370 #[serde(default, skip_serializing_if = "Option::is_none")]
2373 pub x: Option<f32>,
2374 #[serde(default, skip_serializing_if = "Option::is_none")]
2377 pub y: Option<f32>,
2378 #[serde(default, skip_serializing_if = "Option::is_none")]
2382 pub client_x: Option<f32>,
2383 #[serde(default, skip_serializing_if = "Option::is_none")]
2386 pub client_y: Option<f32>,
2387 #[serde(default, skip_serializing_if = "Option::is_none")]
2392 pub button: Option<u8>,
2393 #[serde(default, skip_serializing_if = "Option::is_none")]
2395 pub value: Option<String>,
2396 #[serde(default, skip_serializing_if = "Option::is_none")]
2398 pub selection_start: Option<usize>,
2399 #[serde(default, skip_serializing_if = "Option::is_none")]
2401 pub selection_end: Option<usize>,
2402 #[serde(default, skip_serializing_if = "Option::is_none")]
2405 pub selection_direction: Option<String>,
2406 #[serde(default, skip_serializing_if = "Option::is_none")]
2409 pub composing: Option<bool>,
2410 #[serde(default, skip_serializing_if = "Option::is_none")]
2413 pub scroll_top: Option<f32>,
2414 #[serde(default, skip_serializing_if = "Option::is_none")]
2417 pub scroll_left: Option<f32>,
2418 #[serde(default, skip_serializing_if = "Option::is_none")]
2421 pub delta_x: Option<f32>,
2422 #[serde(default, skip_serializing_if = "Option::is_none")]
2425 pub delta_y: Option<f32>,
2426 #[serde(default, skip_serializing_if = "Option::is_none")]
2430 pub delta_mode: Option<String>,
2431 #[serde(default, skip_serializing_if = "Option::is_none")]
2436 pub width: Option<f32>,
2437 #[serde(default, skip_serializing_if = "Option::is_none")]
2440 pub height: Option<f32>,
2441}
2442
2443#[derive(Debug, Clone, Serialize)]
2447#[serde(tag = "t", rename_all = "camelCase")]
2448pub enum Outbound {
2449 UiEvent { event: UiEvent },
2451 Event {
2454 name: String,
2455 value: serde_json::Value,
2456 },
2457 Response { id: u64, result: ResponseResult },
2459 AnimationFinished {
2463 id: crate::animations::SharedId,
2464 token: u64,
2465 finished: bool,
2466 },
2467 Reload,
2469}
2470
2471#[derive(Debug, Clone, Serialize)]
2475#[serde(tag = "status", rename_all = "camelCase")]
2476pub enum ResponseResult {
2477 Ok { value: serde_json::Value },
2478 Err { message: String },
2479}
2480
2481#[cfg(test)]
2482mod tests {
2483 use super::*;
2484
2485 #[cfg(all(feature = "devtools", debug_assertions))]
2490 #[test]
2491 fn op_batch_attributes_decode_warnings() {
2492 crate::diag::decode_report("length", "stale", "stale entry");
2494 let json = r#"[
2495 {"op":"update","id":7,"props":{"style":{"width":"aa16"}}},
2496 {"op":"append","parent":0,"child":7},
2497 {"op":"update","id":9,"props":{"style":{"display":"flexx","padding":"1px bogus"}}}
2498 ]"#;
2499 let batch: OpBatch = serde_json::from_str(json).expect("batch decodes");
2500 assert_eq!(batch.0.len(), 3, "fallbacks must not drop ops");
2501 let warns = crate::diag::take_decode_warnings();
2502 let brief: Vec<_> = warns
2503 .iter()
2504 .map(|w| (w.node, w.kind, w.value.as_str()))
2505 .collect();
2506 assert_eq!(
2507 brief,
2508 vec![
2509 (Some(7), "length", "aa16"),
2510 (Some(9), "display", "flexx"),
2511 (Some(9), "rect", "bogus"),
2512 ],
2513 );
2514 assert!(warns.iter().all(|w| !w.message.is_empty()));
2515 assert!(
2516 crate::diag::take_decode_warnings().is_empty(),
2517 "drain empties the sink"
2518 );
2519 }
2520
2521 #[test]
2523 fn deserializes_editable_text_create() {
2524 let json = r#"{"op":"create","id":7,"kind":"editableText","props":{
2525 "value":"hi","maxLength":40,"multiline":true,"onChange":true,
2526 "autofocus":true,"selectionStart":0,"selectionEnd":2,
2527 "ariaLabel":"Name","onSelect":true,"onFocus":true,"onBlur":true,
2528 "focusStyle":{"borderColor":"white"}}}"#;
2529 match serde_json::from_str::<Op>(json).expect("valid op") {
2530 Op::Create {
2531 id, kind, props, ..
2532 } => {
2533 assert_eq!(id, 7);
2534 assert_eq!(kind, "editableText");
2535 assert_eq!(props.value.as_deref(), Some("hi"));
2536 assert_eq!(props.max_length, Some(40));
2537 assert!(props.multiline);
2538 assert!(props.on_change);
2539 assert!(props.autofocus);
2540 assert_eq!(props.selection_start, Some(0));
2541 assert_eq!(props.selection_end, Some(2));
2542 assert_eq!(props.aria_label.as_deref(), Some("Name"));
2543 assert!(props.on_select);
2544 assert!(props.on_focus);
2545 assert!(props.on_blur);
2546 assert!(props.focus_style.is_some());
2547 }
2548 other => panic!("expected create, got {other:?}"),
2549 }
2550 }
2551
2552 #[test]
2555 fn deserializes_transform_opacity_and_transition() {
2556 let s: Style = serde_json::from_str(
2557 r#"{
2558 "transform": { "scale": 0.95, "translateX": 4, "translateY": "50%" },
2559 "opacity": 0.5,
2560 "transition": { "transform": { "duration": 0.15, "easing": "easeOut" } }
2561 }"#,
2562 )
2563 .expect("style decodes");
2564 let t = s.transform.expect("transform present");
2565 assert_eq!(t.scale, Some(0.95));
2566 assert_eq!(t.translate_x, Some(Length::Px(4.0)));
2568 assert_eq!(t.translate_y, Some(Length::Percent(50.0)));
2569 assert_eq!(t.scale_x, None);
2570 assert_eq!(s.opacity, Some(0.5));
2571 let transition = s.transition.expect("transition present");
2572 assert!(transition.for_transform().is_some());
2573 assert!(transition.for_opacity().is_none());
2574 }
2575
2576 #[test]
2579 fn angle_units() {
2580 use std::f32::consts::{PI, TAU};
2581 let parse = |v: serde_json::Value| serde_json::from_value::<Angle>(v).unwrap().radians();
2582 assert!((parse(serde_json::json!(180)) - PI).abs() < 1e-5);
2583 assert!((parse(serde_json::json!("180deg")) - PI).abs() < 1e-5);
2584 assert!((parse(serde_json::json!("3.14159rad")) - PI).abs() < 1e-4);
2585 assert!((parse(serde_json::json!("0.5turn")) - PI).abs() < 1e-5);
2586 assert!((parse(serde_json::json!("400grad")) - TAU).abs() < 1e-5);
2587 }
2588
2589 #[test]
2592 fn border_color_scalar_and_per_side() {
2593 let uniform: Style =
2595 serde_json::from_str(r#"{ "borderColor": "white" }"#).expect("scalar decodes");
2596 let bc = uniform.border_color.expect("border_color present");
2597 assert_eq!(bc.top.as_deref(), Some("white"));
2598 assert_eq!(bc.right.as_deref(), Some("white"));
2599 assert_eq!(bc.bottom.as_deref(), Some("white"));
2600 assert_eq!(bc.left.as_deref(), Some("white"));
2601
2602 let sided: Style =
2604 serde_json::from_str(r##"{ "borderColor": { "top": "#f00", "left": "blue" } }"##)
2605 .expect("object decodes");
2606 let bc = sided.border_color.expect("border_color present");
2607 assert_eq!(bc.top.as_deref(), Some("#f00"));
2608 assert_eq!(bc.left.as_deref(), Some("blue"));
2609 assert_eq!(bc.right, None);
2610 assert_eq!(bc.bottom, None);
2611
2612 let bogus: Style =
2616 serde_json::from_str(r#"{ "borderColor": { "middle": "red", "top": "blue" } }"#)
2617 .expect("unknown side key must not abort deserialization");
2618 let bc = bogus.border_color.expect("border_color present");
2619 assert_eq!(bc.top.as_deref(), Some("blue"));
2620 assert_eq!(bc.right, None);
2621 assert_eq!(bc.bottom, None);
2622 assert_eq!(bc.left, None);
2623 }
2624
2625 #[test]
2629 fn bad_unit_values_fall_back_instead_of_aborting() {
2630 let s: Style = serde_json::from_str(r#"{ "width": "100pixels", "height": "40px" }"#)
2632 .expect("a bad length must not abort deserialization");
2633 assert_eq!(s.width, Some(Length::default()));
2634 assert_eq!(s.height, Some(Length::Px(40.0)));
2635
2636 let s: Style = serde_json::from_str(r#"{ "fontSize": "16pxx" }"#)
2638 .expect("bad fontSize must not abort");
2639 assert_eq!(s.font_size, Some(FontSize::Px(0.0)));
2640
2641 let t: Transform = serde_json::from_str(r#"{ "rotate": "45degg", "translateX": "50%" }"#)
2643 .expect("bad angle must not abort");
2644 assert_eq!(t.rotate, Some(Angle::default()));
2645 assert_eq!(t.translate_x, Some(Length::Percent(50.0)));
2646
2647 let s: Style =
2651 serde_json::from_str(r#"{ "padding": "16asd" }"#).expect("bad rect must not abort");
2652 assert_eq!(s.padding, Some(Rect::default()));
2653
2654 let s: Style = serde_json::from_str(r#"{ "padding": "8px 16asd" }"#)
2655 .expect("partial-bad rect must not abort");
2656 assert_eq!(
2658 s.padding,
2659 Some(Rect {
2660 top: Length::Px(8.0),
2661 bottom: Length::Px(8.0),
2662 right: Length::default(),
2663 left: Length::default(),
2664 })
2665 );
2666
2667 let s: Style = serde_json::from_str(r#"{ "padding": "8px 16px" }"#)
2668 .expect("valid two-value shorthand decodes");
2669 assert_eq!(
2670 s.padding,
2671 Some(Rect {
2672 top: Length::Px(8.0),
2673 bottom: Length::Px(8.0),
2674 right: Length::Px(16.0),
2675 left: Length::Px(16.0),
2676 })
2677 );
2678
2679 let s: Style = serde_json::from_str(r#"{ "padding": "1px 2px 3px 4px 5px" }"#)
2681 .expect("bad value-count must not abort");
2682 assert_eq!(s.padding, Some(Rect::default()));
2683 }
2684
2685 #[test]
2691 fn keyword_fields_decode_to_bevy_enums() {
2692 let s: Style = serde_json::from_value(serde_json::json!({
2693 "display": "grid",
2694 "alignItems": "start",
2695 "alignSelf": "flexStart",
2696 "alignContent": "spaceBetween",
2697 "justifyContent": "flexEnd",
2698 "flexWrap": "nowrap",
2699 "focusPolicy": "block",
2700 "textAlign": "justify",
2701 "lineBreak": "anyCharacter",
2702 }))
2703 .expect("keyword style decodes");
2704 assert_eq!(s.display, Some(Display::Grid));
2705 assert_eq!(s.align_items, Some(AlignItems::Start));
2706 assert_eq!(s.align_self, Some(AlignSelf::FlexStart));
2707 assert_eq!(s.align_content, Some(AlignContent::SpaceBetween));
2708 assert_eq!(s.justify_content, Some(JustifyContent::FlexEnd));
2709 assert_eq!(s.flex_wrap, Some(FlexWrap::NoWrap));
2710 assert_eq!(s.focus_policy, Some(FocusPolicy::Block));
2711 assert_eq!(s.text_align, Some(Justify::Justified));
2712 assert_eq!(s.line_break, Some(LineBreak::AnyCharacter));
2713
2714 let s: Style = serde_json::from_value(serde_json::json!({
2715 "alignItems": "flexStart",
2716 "justifyContent": "start",
2717 "boxSizing": "border-box",
2719 "flexWrap": "noWrap",
2720 }))
2721 .expect("alias keywords decode");
2722 assert_eq!(s.align_items, Some(AlignItems::FlexStart));
2723 assert_eq!(s.justify_content, Some(JustifyContent::Start));
2724 assert_eq!(s.box_sizing, Some(BoxSizing::BorderBox));
2725 assert_eq!(s.flex_wrap, Some(FlexWrap::NoWrap));
2726 }
2727
2728 #[test]
2732 fn unknown_enum_keywords_fall_back_to_default() {
2733 let s: Style = serde_json::from_value(serde_json::json!({
2734 "display": "flx",
2735 "alignItems": "centre",
2736 "flexDirection": "sideways",
2737 "textAlign": "middle",
2738 "fontWeight": "heavyish",
2739 "focusPolicy": "weird",
2740 "lineBreak": "wordBoundary",
2742 }))
2743 .expect("bad keywords must not abort deserialization");
2744 assert_eq!(s.display, Some(Display::default()));
2745 assert_eq!(s.align_items, Some(AlignItems::default()));
2746 assert_eq!(s.flex_direction, Some(FlexDirection::default()));
2747 assert_eq!(s.text_align, Some(Justify::default()));
2748 assert_eq!(s.font_weight, Some(FontWeight::NORMAL));
2749 assert_eq!(s.focus_policy, Some(FocusPolicy::Pass));
2750 assert_eq!(s.line_break, Some(LineBreak::WordBoundary));
2751 }
2752
2753 #[test]
2755 fn font_weight_keywords_and_numeric() {
2756 let fw = |v: serde_json::Value| {
2757 serde_json::from_value::<Style>(serde_json::json!({ "fontWeight": v }))
2758 .expect("fontWeight decodes")
2759 .font_weight
2760 };
2761 assert_eq!(fw("bold".into()), Some(FontWeight::BOLD));
2762 assert_eq!(fw("600".into()), Some(FontWeight(600)));
2763 assert_eq!(fw("thin".into()), Some(FontWeight::THIN));
2764 }
2765
2766 #[test]
2768 fn grid_templates_and_placement_decode() {
2769 let s: Style = serde_json::from_value(serde_json::json!({
2770 "gridTemplateColumns": "1fr 2fr 100px",
2771 "gridTemplateRows": "repeat(3, 1fr)",
2772 "gridAutoRows": "auto 40px",
2773 }))
2774 .expect("grid template decodes");
2775 assert_eq!(s.grid_template_columns.map(|t| t.len()), Some(3));
2776 assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(1));
2777 assert_eq!(s.grid_auto_rows.map(|t| t.len()), Some(2));
2778
2779 let s: Style =
2781 serde_json::from_value(serde_json::json!({ "gridTemplateRows": "1fr bogus 2fr" }))
2782 .expect("bad track must not abort");
2783 assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(2));
2784
2785 let placed = |v: &str| {
2786 let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
2787 .expect("grid placement decodes");
2788 format!("{:?}", s.grid_row.unwrap())
2789 };
2790 let expect = |p: GridPlacement| format!("{p:?}");
2791 assert_eq!(placed("1 / 3"), expect(GridPlacement::start_end(1, 3)));
2792 assert_eq!(placed("span 2"), expect(GridPlacement::span(2)));
2793 assert_eq!(
2794 placed("2 / span 3"),
2795 expect(GridPlacement::start_span(2, 3))
2796 );
2797 assert_eq!(placed("2 / 2"), expect(GridPlacement::start_end(2, 2)));
2798 assert_eq!(placed("-1"), expect(GridPlacement::start(-1)));
2799 assert_eq!(placed("2 / auto"), expect(GridPlacement::start(2)));
2800 assert_eq!(placed("auto / 3"), expect(GridPlacement::end(3)));
2801 }
2802
2803 #[test]
2807 fn grid_placement_zero_falls_back_to_auto() {
2808 let placed = |v: &str| {
2809 let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
2810 .expect("zero placement must not abort");
2811 format!("{:?}", s.grid_row.unwrap())
2812 };
2813 let auto = format!("{:?}", GridPlacement::auto());
2814 for s in ["0", "span 0", "0 / 2", "2 / 0", "0 / span 2", "2 / span 0"] {
2815 assert_eq!(placed(s), auto, "input {s:?}");
2816 }
2817 assert_eq!(placed("garbage"), auto);
2819 }
2820
2821 #[test]
2825 fn deserializes_filter_functions() {
2826 let s: Style = serde_json::from_str(
2827 r#"{ "filter": {
2828 "blur": "4px", "brightness": 1.2, "grayscale": 1,
2829 "saturate": 0.5, "hueRotate": 90
2830 } }"#,
2831 )
2832 .expect("filter decodes");
2833 let f = s.filter.expect("filter present");
2834 assert_eq!(f.blur, Some(Length::Px(4.0)));
2835 assert_eq!(f.brightness, Some(1.2));
2836 assert_eq!(f.grayscale, Some(1.0));
2837 assert_eq!(f.saturate, Some(0.5));
2838 assert!((f.hue_rotate.unwrap().radians() - std::f32::consts::FRAC_PI_2).abs() < 1e-5);
2839 assert_eq!(f.contrast, None);
2841 assert_eq!(f.sepia, None);
2842 assert_eq!(f.invert, None);
2843
2844 let s: Style = serde_json::from_str(r#"{ "filter": { "blur": "4pxx" }, "opacity": 0.5 }"#)
2846 .expect("a bad filter unit must not abort the style");
2847 assert_eq!(s.filter.unwrap().blur, Some(Length::default()));
2848 assert_eq!(s.opacity, Some(0.5));
2849 }
2850
2851 #[test]
2854 fn serializes_change_event_with_value() {
2855 let ev = UiEvent {
2856 id: 7,
2857 kind: "change".into(),
2858 value: Some("hello".into()),
2859 ..Default::default()
2860 };
2861 let v = serde_json::to_value(&ev).expect("serializable");
2862 assert_eq!(v["kind"], "change");
2863 assert_eq!(v["value"], "hello");
2864 assert!(v.get("clientX").is_none(), "pointer fields omitted");
2865 assert!(v.get("button").is_none(), "button omitted on text events");
2866 }
2867
2868 #[test]
2871 fn serializes_pointer_event_with_button() {
2872 let ev = UiEvent {
2873 id: 3,
2874 kind: "pointerDown".into(),
2875 button: Some(2),
2876 ..Default::default()
2877 };
2878 let v = serde_json::to_value(&ev).expect("serializable");
2879 assert_eq!(v["kind"], "pointerDown");
2880 assert_eq!(v["button"], 2);
2881 }
2882
2883 #[test]
2887 fn style_field_table_is_complete() {
2888 macro_rules! build_full {
2889 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
2890 Style { $($f: None,)* }
2891 };
2892 }
2893 let _style: Style = with_style_fields!(build_full);
2894 }
2895
2896 #[test]
2900 fn style_wire_names_match_serde_rename() {
2901 fn camel(s: &str) -> String {
2902 let mut out = String::new();
2903 let mut up = false;
2904 for c in s.chars() {
2905 if c == '_' {
2906 up = true;
2907 } else if up {
2908 out.extend(c.to_uppercase());
2909 up = false;
2910 } else {
2911 out.push(c);
2912 }
2913 }
2914 out
2915 }
2916 macro_rules! check {
2917 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
2918 $( assert_eq!(camel(stringify!($f)), $name, "table wire name for `{}`", stringify!($f)); )*
2919 };
2920 }
2921 with_style_fields!(check);
2922 }
2923
2924 fn props(json: serde_json::Value) -> Props {
2925 serde_json::from_value(json).expect("valid props")
2926 }
2927
2928 #[test]
2930 fn merge_delta_sets_and_preserves() {
2931 let mut cached = props(serde_json::json!({
2932 "style": { "backgroundColor": "red", "outline": { "color": "white" } },
2933 "hoverStyle": { "backgroundColor": "blue" },
2934 "onClick": true,
2935 "src": "a.png",
2936 }));
2937 let (dirty, ev) = cached.merge_delta(
2938 props(serde_json::json!({ "style": { "width": 100 } })),
2939 &[],
2940 &[],
2941 );
2942
2943 let style = cached.style.as_ref().unwrap();
2944 assert_eq!(style.width, Some(Length::Px(100.0)));
2945 assert_eq!(style.background_color.as_deref(), Some("red"));
2946 assert!(style.outline.is_some(), "untouched style fields preserved");
2947 assert!(cached.hover_style.is_some(), "untouched props preserved");
2948 assert!(cached.on_click);
2949 assert_eq!(cached.src.as_deref(), Some("a.png"));
2950
2951 assert!(dirty.style.intersects(style_groups::LAYOUT));
2952 assert!(
2953 !dirty
2954 .style
2955 .intersects(style_groups::BACKGROUND | style_groups::OUTLINE),
2956 "untouched groups must stay clean"
2957 );
2958 assert!(!dirty.hover_style && !dirty.pointer && !dirty.image);
2959 assert!(dirty.style.intersects(style_groups::TRANSITION));
2961 assert!(ev.value.is_none() && ev.draw.is_none());
2962 }
2963
2964 #[test]
2967 fn merge_delta_unsets() {
2968 let mut cached = props(serde_json::json!({
2969 "style": { "backgroundColor": "red", "width": 50 },
2970 "hoverStyle": { "backgroundColor": "blue" },
2971 "onClick": true,
2972 }));
2973 let (dirty, _) = cached.merge_delta(
2974 Props::default(),
2975 &["hoverStyle".into(), "onClick".into()],
2976 &["backgroundColor".into()],
2977 );
2978
2979 let style = cached.style.as_ref().unwrap();
2980 assert_eq!(style.background_color, None);
2981 assert_eq!(
2982 style.width,
2983 Some(Length::Px(50.0)),
2984 "other style fields kept"
2985 );
2986 assert!(cached.hover_style.is_none());
2987 assert!(!cached.on_click);
2988 assert!(dirty.style.intersects(style_groups::BACKGROUND));
2989 assert!(!dirty.style.intersects(style_groups::LAYOUT));
2990 assert!(dirty.hover_style && dirty.pointer);
2991 assert!(dirty.any_style_variant());
2992 }
2993
2994 #[test]
3000 fn merge_delta_bool_false_is_noop_off_rides_unset() {
3001 let mut cached = props(serde_json::json!({ "flipX": true, "flipY": true }));
3002
3003 let (dirty, _) = cached.merge_delta(props(serde_json::json!({ "flipX": false })), &[], &[]);
3005 assert!(cached.flip_x, "explicit false in a delta must not clear");
3006 assert!(!dirty.image);
3007
3008 let (dirty, _) = cached.merge_delta(Props::default(), &["flipX".into()], &[]);
3010 assert!(!cached.flip_x);
3011 assert!(cached.flip_y, "sibling flag untouched");
3012 assert!(dirty.image);
3013 }
3014
3015 #[test]
3017 fn merge_delta_unsets_style_wholesale() {
3018 let mut cached = props(serde_json::json!({
3019 "style": { "backgroundColor": "red", "width": 50 },
3020 }));
3021 let (dirty, _) = cached.merge_delta(Props::default(), &["style".into()], &[]);
3022 assert!(cached.style.is_none());
3023 assert_eq!(dirty.style, StyleDirty::ALL);
3024 }
3025
3026 #[test]
3028 fn merge_delta_events_not_cached() {
3029 let mut cached = Props::default();
3030 let (dirty, ev) = cached.merge_delta(
3031 props(serde_json::json!({
3032 "value": "hi", "selectionStart": 1, "selectionEnd": 3,
3033 "scrollTop": 40.0, "scrollLeft": 2.0,
3034 })),
3035 &[],
3036 &[],
3037 );
3038 assert_eq!(ev.value.as_deref(), Some("hi"));
3039 assert_eq!((ev.selection_start, ev.selection_end), (Some(1), Some(3)));
3040 assert_eq!((ev.scroll_top, ev.scroll_left), (Some(40.0), Some(2.0)));
3041 assert!(cached.value.is_none() && cached.scroll_top.is_none());
3042 assert!(cached.selection_start.is_none());
3043 assert!(!dirty.style.any() && !dirty.image && !dirty.anchor);
3045 }
3046
3047 #[test]
3050 fn merge_delta_replaces_variants_atomically() {
3051 let mut cached = props(serde_json::json!({
3052 "hoverStyle": { "backgroundColor": "blue", "width": 10 },
3053 }));
3054 let (dirty, _) = cached.merge_delta(
3055 props(serde_json::json!({ "hoverStyle": { "outline": { "color": "white" } } })),
3056 &[],
3057 &[],
3058 );
3059 let hover = cached.hover_style.as_ref().unwrap();
3060 assert!(hover.outline.is_some());
3061 assert_eq!(hover.background_color, None, "atomic replace, not a merge");
3062 assert_eq!(hover.width, None);
3063 assert!(dirty.hover_style);
3064 }
3065
3066 #[test]
3069 fn merge_delta_ignores_unknown_names() {
3070 let mut cached = props(serde_json::json!({ "style": { "width": 10 } }));
3071 let (dirty, _) = cached.merge_delta(
3072 Props::default(),
3073 &["nope".into(), "value".into()],
3074 &["alsoNope".into()],
3075 );
3076 assert_eq!(cached.style.as_ref().unwrap().width, Some(Length::Px(10.0)));
3077 assert!(!dirty.style.any());
3078 }
3079
3080 #[test]
3082 fn merge_delta_converges() {
3083 let base = serde_json::json!({
3084 "style": { "backgroundColor": "red", "width": 10 }, "onClick": true,
3085 });
3086 let mut two_steps = props(base.clone());
3087 two_steps.merge_delta(
3088 props(serde_json::json!({ "style": { "width": 20 } })),
3089 &[],
3090 &[],
3091 );
3092 two_steps.merge_delta(
3093 props(serde_json::json!({ "style": { "height": 5 } })),
3094 &[],
3095 &["backgroundColor".into()],
3096 );
3097
3098 let mut one_step = props(base);
3099 one_step.merge_delta(
3100 props(serde_json::json!({ "style": { "width": 20, "height": 5 } })),
3101 &[],
3102 &["backgroundColor".into()],
3103 );
3104
3105 let a = two_steps.style.as_ref().unwrap();
3106 let b = one_step.style.as_ref().unwrap();
3107 assert_eq!(a.width, b.width);
3108 assert_eq!(a.height, b.height);
3109 assert_eq!(a.background_color, b.background_color);
3110 assert!(two_steps.on_click && one_step.on_click);
3111 }
3112
3113 #[test]
3115 fn split_events_strips_event_fields() {
3116 let full = props(serde_json::json!({
3117 "style": { "width": 10 }, "onClick": true, "value": "v",
3118 "selectionStart": 0, "selectionEnd": 1, "scrollTop": 5.0,
3119 }));
3120 let (state, ev) = full.split_events();
3121 assert!(state.style.is_some() && state.on_click);
3122 assert!(state.value.is_none() && state.selection_start.is_none());
3123 assert!(state.scroll_top.is_none());
3124 assert_eq!(ev.value.as_deref(), Some("v"));
3125 assert_eq!(ev.scroll_top, Some(5.0));
3126 }
3127
3128 #[test]
3132 fn deserializes_update_delta_form() {
3133 let minimal: Op = serde_json::from_str(r#"{"op":"update","id":3,"props":{}}"#).unwrap();
3134 match minimal {
3135 Op::Update {
3136 unset, style_unset, ..
3137 } => {
3138 assert!(unset.is_empty() && style_unset.is_empty());
3139 }
3140 other => panic!("expected update, got {other:?}"),
3141 }
3142 let full: Op = serde_json::from_str(
3143 r#"{"op":"update","id":3,"props":{"style":{"width":1}},
3144 "unset":["onClick"],"styleUnset":["backgroundColor"]}"#,
3145 )
3146 .unwrap();
3147 match full {
3148 Op::Update {
3149 unset, style_unset, ..
3150 } => {
3151 assert_eq!(unset, vec!["onClick"]);
3152 assert_eq!(style_unset, vec!["backgroundColor"]);
3153 }
3154 other => panic!("expected update, got {other:?}"),
3155 }
3156 }
3157
3158 #[test]
3162 fn deserializes_draw_op() {
3163 let op: Op = serde_json::from_str(
3164 r##"{"op":"draw","id":7,"cmds":[
3165 {"cmd":"clear"},
3166 {"cmd":"clearRect","x":1.0,"y":2.0,"w":3.0,"h":4.0},
3167 {"cmd":"fillStyle","color":"#f00"}
3168 ]}"##,
3169 )
3170 .unwrap();
3171 match op {
3172 Op::Draw { id, cmds } => {
3173 assert_eq!(id, 7);
3174 assert_eq!(cmds.len(), 3);
3175 assert_eq!(cmds[0], DrawCmd::Clear);
3176 assert_eq!(
3177 cmds[1],
3178 DrawCmd::ClearRect {
3179 x: 1.0,
3180 y: 2.0,
3181 w: 3.0,
3182 h: 4.0
3183 }
3184 );
3185 assert_eq!(
3186 cmds[2],
3187 DrawCmd::FillStyle {
3188 color: "#f00".into()
3189 }
3190 );
3191 }
3192 other => panic!("expected draw, got {other:?}"),
3193 }
3194 }
3195
3196 #[test]
3199 fn serializes_resize_ui_event() {
3200 let v = serde_json::to_value(Outbound::UiEvent {
3201 event: UiEvent {
3202 id: 5,
3203 kind: "resize".into(),
3204 width: Some(300.0),
3205 height: Some(150.0),
3206 ..Default::default()
3207 },
3208 })
3209 .unwrap();
3210 assert_eq!(v["t"], "uiEvent");
3211 let ev = &v["event"];
3212 assert_eq!(ev["id"], 5);
3213 assert_eq!(ev["kind"], "resize");
3214 assert_eq!(ev["width"], 300.0);
3215 assert_eq!(ev["height"], 150.0);
3216 assert!(ev.get("x").is_none() && ev.get("scrollTop").is_none());
3217 }
3218
3219 #[test]
3222 fn merge_delta_on_resize_flag() {
3223 let mut cached = Props::default();
3224 let (dirty, _) =
3225 cached.merge_delta(props(serde_json::json!({ "onResize": true })), &[], &[]);
3226 assert!(cached.on_resize);
3227 assert!(!dirty.pointer && !dirty.scroll_listener);
3228 cached.merge_delta(Props::default(), &["onResize".into()], &[]);
3229 assert!(!cached.on_resize);
3230 }
3231
3232 #[test]
3235 fn merge_delta_wheel_flag() {
3236 let mut cached = Props::default();
3237 let (dirty, _) =
3238 cached.merge_delta(props(serde_json::json!({ "onWheel": true })), &[], &[]);
3239 assert!(cached.on_wheel);
3240 assert!(dirty.wheel);
3241 assert!(!dirty.pointer && !dirty.scroll_listener);
3242
3243 let (dirty, _) = cached.merge_delta(Props::default(), &["onWheel".into()], &[]);
3244 assert!(!cached.on_wheel);
3245 assert!(dirty.wheel);
3246 }
3247
3248 #[test]
3251 fn deserializes_cursor_name() {
3252 let s: Style = serde_json::from_str(r#"{ "cursor": "pointer" }"#).expect("cursor decodes");
3253 assert_eq!(s.cursor.as_deref(), Some("pointer"));
3254
3255 let s: Style =
3256 serde_json::from_str(r#"{ "cursor": "hand" }"#).expect("custom name decodes");
3257 assert_eq!(s.cursor.as_deref(), Some("hand"));
3258 }
3259
3260 #[test]
3263 fn merge_delta_cursor_group() {
3264 let mut cached = Props::default();
3265 let (dirty, _) = cached.merge_delta(
3266 props(serde_json::json!({ "style": { "cursor": "pointer" } })),
3267 &[],
3268 &[],
3269 );
3270 assert_eq!(
3271 cached.style.as_ref().unwrap().cursor.as_deref(),
3272 Some("pointer")
3273 );
3274 assert!(dirty.style.intersects(style_groups::CURSOR));
3275 assert!(!dirty.style.intersects(style_groups::LAYOUT));
3276
3277 let (dirty, _) = cached.merge_delta(Props::default(), &[], &["cursor".into()]);
3278 assert_eq!(cached.style.as_ref().unwrap().cursor, None);
3279 assert!(dirty.style.intersects(style_groups::CURSOR));
3280 }
3281}