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)]
252 pub anchor: Option<crate::anchor::Anchor>,
253
254 #[serde(default)]
258 pub src: Option<String>,
259 #[serde(default)]
261 pub tint: Option<String>,
262 #[serde(default)]
264 pub flip_x: bool,
265 #[serde(default)]
267 pub flip_y: bool,
268 #[serde(default)]
271 pub image_mode: Option<ImageMode>,
272 #[serde(default)]
276 pub source_rect: Option<SourceRect>,
277 #[serde(default)]
280 pub atlas: Option<AtlasSpec>,
281 #[serde(default)]
284 pub visual_box: Option<String>,
285
286 #[serde(default)]
294 pub draw: Option<Vec<DrawCmd>>,
295 #[serde(default)]
300 pub on_resize: bool,
301
302 #[serde(default)]
308 pub target: Option<String>,
309
310 #[serde(default)]
315 pub value: Option<String>,
316 #[serde(default)]
318 pub max_length: Option<usize>,
319 #[serde(default)]
321 pub multiline: bool,
322 #[serde(default)]
324 pub on_change: bool,
325 #[serde(default)]
327 pub autofocus: bool,
328 #[serde(default)]
332 pub selection_start: Option<usize>,
333 #[serde(default)]
335 pub selection_end: Option<usize>,
336 #[serde(default)]
338 pub aria_label: Option<String>,
339 #[serde(default)]
341 pub on_select: bool,
342 #[serde(default)]
344 pub on_focus: bool,
345 #[serde(default)]
347 pub on_blur: bool,
348}
349
350pub use crate::canvas::DrawCmd;
354
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
362pub enum LayerCache {
363 #[default]
364 Auto,
365 Always,
366 Never,
367}
368
369#[derive(Debug, Clone, Default, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct Style {
383 #[serde(default, deserialize_with = "de_display")]
385 pub display: Option<Display>,
386 #[serde(default, deserialize_with = "de_box_sizing")]
387 pub box_sizing: Option<BoxSizing>,
388 #[serde(default, deserialize_with = "de_position_type")]
389 pub position_type: Option<PositionType>,
390 #[serde(default, deserialize_with = "de_overflow_axis")]
391 pub overflow_x: Option<OverflowAxis>,
392 #[serde(default, deserialize_with = "de_overflow_axis")]
393 pub overflow_y: Option<OverflowAxis>,
394 #[serde(default)]
395 pub scrollbar_width: Option<f32>,
396
397 #[serde(default)]
399 pub left: Option<Animatable<Length>>,
400 #[serde(default)]
401 pub right: Option<Animatable<Length>>,
402 #[serde(default)]
403 pub top: Option<Animatable<Length>>,
404 #[serde(default)]
405 pub bottom: Option<Animatable<Length>>,
406
407 #[serde(default)]
409 pub width: Option<Animatable<Length>>,
410 #[serde(default)]
411 pub height: Option<Animatable<Length>>,
412 #[serde(default)]
413 pub min_width: Option<Animatable<Length>>,
414 #[serde(default)]
415 pub min_height: Option<Animatable<Length>>,
416 #[serde(default)]
417 pub max_width: Option<Animatable<Length>>,
418 #[serde(default)]
419 pub max_height: Option<Animatable<Length>>,
420 #[serde(default)]
421 pub aspect_ratio: Option<Animatable<f32>>,
422
423 #[serde(default, deserialize_with = "de_align_items")]
425 pub align_items: Option<AlignItems>,
426 #[serde(default, deserialize_with = "de_justify_items")]
427 pub justify_items: Option<JustifyItems>,
428 #[serde(default, deserialize_with = "de_align_self")]
429 pub align_self: Option<AlignSelf>,
430 #[serde(default, deserialize_with = "de_justify_self")]
431 pub justify_self: Option<JustifySelf>,
432 #[serde(default, deserialize_with = "de_align_content")]
433 pub align_content: Option<AlignContent>,
434 #[serde(default, deserialize_with = "de_justify_content")]
435 pub justify_content: Option<JustifyContent>,
436
437 #[serde(default)]
439 pub margin: Option<Rect>,
440 #[serde(default)]
441 pub padding: Option<Rect>,
442 #[serde(default)]
443 pub border: Option<Rect>,
444
445 #[serde(default, deserialize_with = "de_flex_direction")]
447 pub flex_direction: Option<FlexDirection>,
448 #[serde(default, deserialize_with = "de_flex_wrap")]
449 pub flex_wrap: Option<FlexWrap>,
450 #[serde(default)]
451 pub flex_grow: Option<f32>,
452 #[serde(default)]
453 pub flex_shrink: Option<f32>,
454 #[serde(default)]
455 pub flex_basis: Option<Animatable<Length>>,
456 #[serde(default)]
457 pub gap: Option<Animatable<Length>>,
458 #[serde(default)]
459 pub row_gap: Option<Animatable<Length>>,
460 #[serde(default)]
461 pub column_gap: Option<Animatable<Length>>,
462
463 #[serde(default, deserialize_with = "de_grid_auto_flow")]
465 pub grid_auto_flow: Option<GridAutoFlow>,
466 #[serde(default, deserialize_with = "de_grid_template")]
468 pub grid_template_rows: Option<Vec<RepeatedGridTrack>>,
469 #[serde(default, deserialize_with = "de_grid_template")]
470 pub grid_template_columns: Option<Vec<RepeatedGridTrack>>,
471 #[serde(default, deserialize_with = "de_grid_auto_tracks")]
473 pub grid_auto_rows: Option<Vec<GridTrack>>,
474 #[serde(default, deserialize_with = "de_grid_auto_tracks")]
475 pub grid_auto_columns: Option<Vec<GridTrack>>,
476 #[serde(default, deserialize_with = "de_grid_placement")]
478 pub grid_row: Option<GridPlacement>,
479 #[serde(default, deserialize_with = "de_grid_placement")]
480 pub grid_column: Option<GridPlacement>,
481
482 #[serde(default)]
486 pub background_color: Option<Animatable<String>>,
487 #[serde(default)]
492 pub border_color: Option<Animatable<BorderColorSpec>>,
493 #[serde(default)]
496 pub border_radius: Option<Rect>,
497 #[serde(default)]
498 pub outline: Option<OutlineSpec>,
499 #[serde(default)]
500 pub box_shadow: Option<BoxShadowList>,
501 #[serde(default)]
516 pub filter: Option<crate::filters::FilterChain>,
517 #[serde(default)]
526 pub backdrop_filter: Option<crate::filters::FilterChain>,
527 #[serde(default)]
531 pub background_gradient: Option<GradientList>,
532 #[serde(default)]
535 pub border_gradient: Option<GradientList>,
536 #[serde(default)]
537 pub z_index: Option<i32>,
538 #[serde(default)]
542 pub global_z_index: Option<i32>,
543 #[serde(default, deserialize_with = "de_focus_policy")]
549 pub focus_policy: Option<FocusPolicy>,
550 #[serde(default)]
558 pub cursor: Option<String>,
559
560 #[serde(default)]
565 pub transform: Option<Transform>,
566 #[serde(default)]
575 pub transform3d: Option<Transform3d>,
576 #[serde(default)]
582 pub opacity: Option<Animatable<f32>>,
583 #[serde(default)]
589 pub group_alpha: Option<bool>,
590 #[serde(default, deserialize_with = "de_layer_cache")]
599 pub cache: Option<LayerCache>,
600 #[serde(default)]
605 pub transition: Option<crate::transition::Transition>,
606
607 #[serde(default)]
612 pub scrollbar: Option<crate::scrollbar::ScrollbarSpec>,
613
614 #[serde(default)]
617 pub color: Option<Animatable<String>>,
618 #[serde(default)]
621 pub font_size: Option<FontSize>,
622 #[serde(default, deserialize_with = "de_font_weight")]
625 pub font_weight: Option<FontWeight>,
626 #[serde(default)]
630 pub font_family: Option<String>,
631 #[serde(default, deserialize_with = "de_text_align")]
634 pub text_align: Option<Justify>,
635 #[serde(default)]
638 pub line_height: Option<LineHeightSpec>,
639 #[serde(default)]
642 pub letter_spacing: Option<LetterSpacingSpec>,
643 #[serde(default)]
645 pub text_shadow: Option<TextShadowSpec>,
646 #[serde(default, deserialize_with = "de_line_break")]
650 pub line_break: Option<LineBreak>,
651}
652
653pub mod style_groups {
659 pub const LAYOUT: u32 = 1 << 0;
661 pub const BACKGROUND: u32 = 1 << 1;
663 pub const TRANSFORM: u32 = 1 << 2;
665 pub const BORDER_COLOR: u32 = 1 << 3;
667 pub const OUTLINE: u32 = 1 << 4;
669 pub const BOX_SHADOW: u32 = 1 << 5;
671 pub const BG_GRADIENT: u32 = 1 << 6;
673 pub const BORDER_GRADIENT: u32 = 1 << 7;
675 pub const TEXT_SHADOW: u32 = 1 << 8;
677 pub const Z_INDEX: u32 = 1 << 9;
679 pub const GLOBAL_Z_INDEX: u32 = 1 << 10;
681 pub const FOCUS_POLICY: u32 = 1 << 11;
683 pub const FILTER: u32 = 1 << 12;
686 pub const TRANSITION: u32 = 1 << 13;
691 pub const SCROLL_TRANSITION: u32 = 1 << 14;
693 pub const TEXT: u32 = 1 << 15;
697 pub const TEXT_LAYOUT: u32 = 1 << 16;
699 pub const CURSOR: u32 = 1 << 17;
702 pub const SCROLLBAR: u32 = 1 << 18;
707 pub const LAYER: u32 = 1 << 19;
713 pub const TRANSFORM3D: u32 = 1 << 20;
717 pub const BACKDROP: u32 = 1 << 21;
723}
724
725macro_rules! with_style_fields {
744 ($cb:ident) => {
745 $cb! {
746 (display, "display", (LAYOUT), overlay),
747 (box_sizing, "boxSizing", (LAYOUT), overlay),
748 (position_type, "positionType", (LAYOUT), overlay),
749 (overflow_x, "overflowX", (LAYOUT), overlay),
750 (overflow_y, "overflowY", (LAYOUT), overlay),
751 (scrollbar_width, "scrollbarWidth", (LAYOUT), overlay),
752 (left, "left", (LAYOUT), overlay),
753 (right, "right", (LAYOUT), overlay),
754 (top, "top", (LAYOUT), overlay),
755 (bottom, "bottom", (LAYOUT), overlay),
756 (width, "width", (LAYOUT | TRANSITION), overlay),
757 (height, "height", (LAYOUT | TRANSITION), overlay),
758 (min_width, "minWidth", (LAYOUT), overlay),
759 (min_height, "minHeight", (LAYOUT), overlay),
760 (max_width, "maxWidth", (LAYOUT | TRANSITION), overlay),
761 (max_height, "maxHeight", (LAYOUT | TRANSITION), overlay),
762 (aspect_ratio, "aspectRatio", (LAYOUT), overlay),
763 (align_items, "alignItems", (LAYOUT), overlay),
764 (justify_items, "justifyItems", (LAYOUT), overlay),
765 (align_self, "alignSelf", (LAYOUT), overlay),
766 (justify_self, "justifySelf", (LAYOUT), overlay),
767 (align_content, "alignContent", (LAYOUT), overlay),
768 (justify_content, "justifyContent", (LAYOUT), overlay),
769 (margin, "margin", (LAYOUT), overlay),
770 (padding, "padding", (LAYOUT), overlay),
771 (border, "border", (LAYOUT), overlay),
772 (flex_direction, "flexDirection", (LAYOUT), overlay),
773 (flex_wrap, "flexWrap", (LAYOUT), overlay),
774 (flex_grow, "flexGrow", (LAYOUT), overlay),
775 (flex_shrink, "flexShrink", (LAYOUT), overlay),
776 (flex_basis, "flexBasis", (LAYOUT), overlay),
777 (gap, "gap", (LAYOUT), overlay),
778 (row_gap, "rowGap", (LAYOUT), overlay),
779 (column_gap, "columnGap", (LAYOUT), overlay),
780 (grid_auto_flow, "gridAutoFlow", (LAYOUT), overlay),
781 (grid_template_rows, "gridTemplateRows", (LAYOUT), overlay),
782 (grid_template_columns, "gridTemplateColumns", (LAYOUT), overlay),
783 (grid_auto_rows, "gridAutoRows", (LAYOUT), overlay),
784 (grid_auto_columns, "gridAutoColumns", (LAYOUT), overlay),
785 (grid_row, "gridRow", (LAYOUT), overlay),
786 (grid_column, "gridColumn", (LAYOUT), overlay),
787 (background_color, "backgroundColor", (BACKGROUND | TRANSITION), overlay),
788 (border_color, "borderColor", (BORDER_COLOR), overlay),
789 (border_radius, "borderRadius", (LAYOUT), overlay),
790 (outline, "outline", (OUTLINE), overlay),
791 (box_shadow, "boxShadow", (BOX_SHADOW), overlay),
792 (filter, "filter", (FILTER | LAYER), overlay),
793 (backdrop_filter, "backdropFilter", (BACKDROP | LAYER), overlay),
794 (background_gradient, "backgroundGradient", (BG_GRADIENT), overlay),
795 (border_gradient, "borderGradient", (BORDER_GRADIENT), overlay),
796 (z_index, "zIndex", (Z_INDEX), overlay),
797 (global_z_index, "globalZIndex", (GLOBAL_Z_INDEX), overlay),
798 (focus_policy, "focusPolicy", (FOCUS_POLICY), no_overlay),
799 (cursor, "cursor", (CURSOR), overlay),
800 (scrollbar, "scrollbar", (SCROLLBAR | LAYOUT), overlay),
801 (
802 transform,
803 "transform",
804 (TRANSFORM | TRANSITION),
805 overlay
806 ),
807 (
808 transform3d,
809 "transform3d",
810 (TRANSFORM3D | LAYER | TRANSITION),
811 overlay
812 ),
813 (
814 opacity,
815 "opacity",
816 (BACKGROUND | BG_GRADIENT | BORDER_GRADIENT | TEXT_SHADOW | TRANSITION | TEXT
817 | LAYER),
818 overlay
819 ),
820 (group_alpha, "groupAlpha", (LAYER), no_overlay),
821 (cache, "cache", (LAYER), no_overlay),
822 (
823 transition,
824 "transition",
825 (TRANSITION | SCROLL_TRANSITION),
826 overlay
827 ),
828 (color, "color", (TEXT), overlay),
829 (font_size, "fontSize", (TEXT), overlay),
830 (font_weight, "fontWeight", (TEXT), overlay),
831 (font_family, "fontFamily", (TEXT), overlay),
832 (text_align, "textAlign", (TEXT_LAYOUT), overlay),
833 (line_height, "lineHeight", (TEXT), overlay),
834 (letter_spacing, "letterSpacing", (TEXT), overlay),
835 (text_shadow, "textShadow", (TEXT_SHADOW), overlay),
836 (line_break, "lineBreak", (TEXT_LAYOUT), overlay),
837 }
838 };
839}
840pub(crate) use with_style_fields;
841
842#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
845pub struct StyleDirty(pub u32);
846
847impl StyleDirty {
848 pub const NONE: Self = Self(0);
850 pub const ALL: Self = Self(u32::MAX);
852
853 pub fn intersects(self, groups: u32) -> bool {
855 self.0 & groups != 0
856 }
857
858 pub fn any(self) -> bool {
860 self.0 != 0
861 }
862}
863
864#[derive(Debug, Clone, Copy, Default)]
868pub struct PropsDirty {
869 pub style: StyleDirty,
871 pub hover_style: bool,
873 pub press_style: bool,
875 pub focus_style: bool,
877 pub pointer: bool,
879 pub scroll_listener: bool,
881 pub wheel: bool,
883 pub scroll_step: bool,
885 pub anchor: bool,
887 pub image: bool,
890 pub target: bool,
892 pub editable_handlers: bool,
895 pub aria_label: bool,
897}
898
899impl PropsDirty {
900 pub fn any_style_variant(&self) -> bool {
903 self.style.any() || self.hover_style || self.press_style || self.focus_style
904 }
905}
906
907#[derive(Debug, Default)]
912pub struct UpdateEvents {
913 pub value: Option<String>,
915 pub selection_start: Option<usize>,
917 pub selection_end: Option<usize>,
919 pub scroll_top: Option<f32>,
921 pub scroll_left: Option<f32>,
923 pub draw: Option<Vec<DrawCmd>>,
925}
926
927impl Style {
928 pub(crate) fn overlay_delta(&mut self, delta: &Style) -> u32 {
934 let mut groups = 0u32;
935 macro_rules! merge_field {
936 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
937 $(
938 if delta.$f.is_some() {
939 self.$f = delta.$f.clone();
940 groups |= {
941 use style_groups::*;
942 $g
943 };
944 }
945 )*
946 };
947 }
948 with_style_fields!(merge_field);
949 groups
950 }
951
952 pub(crate) fn unset_field(&mut self, wire_name: &str) -> Option<u32> {
955 macro_rules! unset_match {
956 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
957 match wire_name {
958 $(
959 $name => {
960 self.$f = None;
961 Some({
962 use style_groups::*;
963 $g
964 })
965 }
966 )*
967 _ => {
968 tracing::warn!(
969 target: "bevy_react",
970 "unknown style field {wire_name:?} in styleUnset; ignoring"
971 );
972 None
973 }
974 }
975 };
976 }
977 with_style_fields!(unset_match)
978 }
979}
980
981impl Props {
982 pub fn all_styles(&self) -> impl Iterator<Item = &Style> {
988 [
989 &self.style,
990 &self.hover_style,
991 &self.press_style,
992 &self.focus_style,
993 ]
994 .into_iter()
995 .flatten()
996 }
997
998 pub fn split_events(mut self) -> (Props, UpdateEvents) {
1002 let events = UpdateEvents {
1003 value: self.value.take(),
1004 selection_start: self.selection_start.take(),
1005 selection_end: self.selection_end.take(),
1006 scroll_top: self.scroll_top.take(),
1007 scroll_left: self.scroll_left.take(),
1008 draw: self.draw.take(),
1009 };
1010 (self, events)
1011 }
1012
1013 pub fn merge_delta(
1018 &mut self,
1019 delta: Props,
1020 unset: &[String],
1021 style_unset: &[String],
1022 ) -> (PropsDirty, UpdateEvents) {
1023 let mut dirty = PropsDirty::default();
1024 let (delta, events) = delta.split_events();
1025
1026 if let Some(style_delta) = &delta.style {
1028 let groups = self
1029 .style
1030 .get_or_insert_default()
1031 .overlay_delta(style_delta);
1032 dirty.style.0 |= groups;
1033 }
1034 if delta.hover_style.is_some() {
1035 self.hover_style = delta.hover_style;
1036 dirty.hover_style = true;
1037 }
1038 if delta.press_style.is_some() {
1039 self.press_style = delta.press_style;
1040 dirty.press_style = true;
1041 }
1042 if delta.focus_style.is_some() {
1043 self.focus_style = delta.focus_style;
1044 dirty.focus_style = true;
1045 }
1046 macro_rules! merge_bool {
1049 ($($f:ident => $flag:ident),* $(,)?) => {
1050 $(
1051 if delta.$f {
1052 self.$f = true;
1053 dirty.$flag = true;
1054 }
1055 )*
1056 };
1057 }
1058 merge_bool!(
1059 on_click => pointer,
1060 on_pointer_down => pointer,
1061 on_pointer_move => pointer,
1062 on_pointer_up => pointer,
1063 on_pointer_enter => pointer,
1064 on_pointer_leave => pointer,
1065 on_scroll => scroll_listener,
1066 on_wheel => wheel,
1067 on_change => editable_handlers,
1068 on_select => editable_handlers,
1069 on_focus => editable_handlers,
1070 on_blur => editable_handlers,
1071 flip_x => image,
1072 flip_y => image,
1073 );
1074 if delta.multiline {
1077 self.multiline = true;
1078 }
1079 if delta.autofocus {
1080 self.autofocus = true;
1081 }
1082 if delta.on_resize {
1085 self.on_resize = true;
1086 }
1087 macro_rules! merge_option {
1088 ($($f:ident => $($flag:ident)?),* $(,)?) => {
1089 $(
1090 if delta.$f.is_some() {
1091 self.$f = delta.$f;
1092 $( dirty.$flag = true; )?
1093 }
1094 )*
1095 };
1096 }
1097 merge_option!(
1098 scroll_step => scroll_step,
1099 anchor => anchor,
1100 src => image,
1101 tint => image,
1102 image_mode => image,
1103 source_rect => image,
1104 atlas => image,
1105 visual_box => image,
1106 target => target,
1107 aria_label => aria_label,
1108 max_length => , );
1110
1111 for name in unset {
1113 match name.as_str() {
1114 "style" => {
1115 self.style = None;
1116 dirty.style = StyleDirty::ALL;
1117 }
1118 "hoverStyle" => {
1119 self.hover_style = None;
1120 dirty.hover_style = true;
1121 }
1122 "pressStyle" => {
1123 self.press_style = None;
1124 dirty.press_style = true;
1125 }
1126 "focusStyle" => {
1127 self.focus_style = None;
1128 dirty.focus_style = true;
1129 }
1130 "onClick" => {
1131 self.on_click = false;
1132 dirty.pointer = true;
1133 }
1134 "onPointerDown" => {
1135 self.on_pointer_down = false;
1136 dirty.pointer = true;
1137 }
1138 "onPointerMove" => {
1139 self.on_pointer_move = false;
1140 dirty.pointer = true;
1141 }
1142 "onPointerUp" => {
1143 self.on_pointer_up = false;
1144 dirty.pointer = true;
1145 }
1146 "onPointerEnter" => {
1147 self.on_pointer_enter = false;
1148 dirty.pointer = true;
1149 }
1150 "onPointerLeave" => {
1151 self.on_pointer_leave = false;
1152 dirty.pointer = true;
1153 }
1154 "onScroll" => {
1155 self.on_scroll = false;
1156 dirty.scroll_listener = true;
1157 }
1158 "onWheel" => {
1159 self.on_wheel = false;
1160 dirty.wheel = true;
1161 }
1162 "onChange" => {
1163 self.on_change = false;
1164 dirty.editable_handlers = true;
1165 }
1166 "onSelect" => {
1167 self.on_select = false;
1168 dirty.editable_handlers = true;
1169 }
1170 "onFocus" => {
1171 self.on_focus = false;
1172 dirty.editable_handlers = true;
1173 }
1174 "onBlur" => {
1175 self.on_blur = false;
1176 dirty.editable_handlers = true;
1177 }
1178 "flipX" => {
1179 self.flip_x = false;
1180 dirty.image = true;
1181 }
1182 "flipY" => {
1183 self.flip_y = false;
1184 dirty.image = true;
1185 }
1186 "multiline" => self.multiline = false,
1187 "autofocus" => self.autofocus = false,
1188 "onResize" => self.on_resize = false,
1189 "scrollStep" => {
1190 self.scroll_step = None;
1191 dirty.scroll_step = true;
1192 }
1193 "anchor" => {
1194 self.anchor = None;
1195 dirty.anchor = true;
1196 }
1197 "src" => {
1198 self.src = None;
1199 dirty.image = true;
1200 }
1201 "tint" => {
1202 self.tint = None;
1203 dirty.image = true;
1204 }
1205 "imageMode" => {
1206 self.image_mode = None;
1207 dirty.image = true;
1208 }
1209 "sourceRect" => {
1210 self.source_rect = None;
1211 dirty.image = true;
1212 }
1213 "atlas" => {
1214 self.atlas = None;
1215 dirty.image = true;
1216 }
1217 "visualBox" => {
1218 self.visual_box = None;
1219 dirty.image = true;
1220 }
1221 "target" => {
1222 self.target = None;
1223 dirty.target = true;
1224 }
1225 "ariaLabel" => {
1226 self.aria_label = None;
1227 dirty.aria_label = true;
1228 }
1229 "maxLength" => self.max_length = None,
1230 "value" | "selectionStart" | "selectionEnd" | "scrollTop" | "scrollLeft"
1233 | "draw" => {
1234 tracing::warn!(
1235 target: "bevy_react",
1236 "event-like prop {name:?} in unset; nothing to reset"
1237 );
1238 }
1239 other => {
1240 tracing::warn!(
1241 target: "bevy_react",
1242 "unknown prop {other:?} in unset; ignoring"
1243 );
1244 }
1245 }
1246 }
1247
1248 if !style_unset.is_empty() {
1251 let style = self.style.get_or_insert_default();
1252 for name in style_unset {
1253 if let Some(groups) = style.unset_field(name) {
1254 dirty.style.0 |= groups;
1255 }
1256 }
1257 }
1258
1259 (dirty, events)
1260 }
1261}
1262
1263#[derive(Debug, Clone, Default, Deserialize)]
1265#[serde(rename_all = "camelCase")]
1266pub struct OutlineSpec {
1267 #[serde(default)]
1268 pub width: Option<Length>,
1269 #[serde(default)]
1270 pub offset: Option<Length>,
1271 #[serde(default)]
1272 pub color: Option<String>,
1273}
1274
1275#[derive(Debug, Clone, Default, Deserialize)]
1277#[serde(rename_all = "camelCase")]
1278pub struct BoxShadowSpec {
1279 #[serde(default)]
1280 pub color: Option<String>,
1281 #[serde(default)]
1282 pub x_offset: Option<Length>,
1283 #[serde(default)]
1284 pub y_offset: Option<Length>,
1285 #[serde(default)]
1286 pub spread_radius: Option<Length>,
1287 #[serde(default)]
1288 pub blur_radius: Option<Length>,
1289}
1290
1291#[derive(Debug, Clone, Deserialize)]
1293#[serde(untagged)]
1294pub enum BoxShadowList {
1295 One(BoxShadowSpec),
1296 Many(Vec<BoxShadowSpec>),
1297}
1298
1299#[derive(Debug, Clone, Deserialize)]
1303#[serde(untagged)]
1304pub enum LineHeightSpec {
1305 Relative(f32),
1306 Px { px: f32 },
1307 Str(String),
1308}
1309
1310#[derive(Debug, Clone, Deserialize)]
1314#[serde(untagged)]
1315pub enum LetterSpacingSpec {
1316 Px(f32),
1317 Rem { rem: f32 },
1318 Str(String),
1319}
1320
1321#[derive(Debug, Clone, Default, Deserialize)]
1325#[serde(rename_all = "camelCase")]
1326pub struct TextShadowSpec {
1327 #[serde(default)]
1328 pub color: Option<String>,
1329 #[serde(default)]
1330 pub offset_x: Option<f32>,
1331 #[serde(default)]
1332 pub offset_y: Option<f32>,
1333}
1334
1335#[derive(Debug, Clone, Deserialize)]
1340#[serde(rename_all = "camelCase")]
1341pub struct GradientStop {
1342 pub color: String,
1343 #[serde(default)]
1344 pub position: Option<Length>,
1345 #[serde(default)]
1346 pub hint: Option<f32>,
1347}
1348
1349#[derive(Debug, Clone, Deserialize)]
1352#[serde(rename_all = "camelCase")]
1353pub struct AngularStop {
1354 pub color: String,
1355 #[serde(default)]
1356 pub angle: Option<Angle>,
1357 #[serde(default)]
1358 pub hint: Option<f32>,
1359}
1360
1361pub type GradientPosition = String;
1364
1365pub type ColorSpace = String;
1369
1370#[derive(Debug, Clone, Deserialize)]
1374#[serde(rename_all = "camelCase")]
1375pub enum RadialShapeSpec {
1376 Keyword(String),
1377 Circle { circle: Length },
1378 Ellipse { ellipse: [Length; 2] },
1379}
1380
1381#[derive(Debug, Clone, Default, Deserialize)]
1382#[serde(rename_all = "camelCase")]
1383pub struct LinearGradientSpec {
1384 #[serde(default)]
1387 pub angle: Option<Angle>,
1388 #[serde(default)]
1389 pub stops: Vec<GradientStop>,
1390 #[serde(default)]
1391 pub color_space: Option<ColorSpace>,
1392}
1393
1394#[derive(Debug, Clone, Default, Deserialize)]
1395#[serde(rename_all = "camelCase")]
1396pub struct RadialGradientSpec {
1397 #[serde(default)]
1398 pub position: Option<GradientPosition>,
1399 #[serde(default)]
1400 pub shape: Option<RadialShapeSpec>,
1401 #[serde(default)]
1402 pub stops: Vec<GradientStop>,
1403 #[serde(default)]
1404 pub color_space: Option<ColorSpace>,
1405}
1406
1407#[derive(Debug, Clone, Default, Deserialize)]
1408#[serde(rename_all = "camelCase")]
1409pub struct ConicGradientSpec {
1410 #[serde(default)]
1412 pub start: Option<Angle>,
1413 #[serde(default)]
1414 pub position: Option<GradientPosition>,
1415 #[serde(default)]
1416 pub stops: Vec<AngularStop>,
1417 #[serde(default)]
1418 pub color_space: Option<ColorSpace>,
1419}
1420
1421#[derive(Debug, Clone, Deserialize)]
1423#[serde(tag = "type", rename_all = "camelCase")]
1424pub enum GradientSpec {
1425 Linear(LinearGradientSpec),
1426 Radial(RadialGradientSpec),
1427 Conic(ConicGradientSpec),
1428}
1429
1430#[derive(Debug, Clone, Deserialize)]
1432#[serde(untagged)]
1433pub enum GradientList {
1434 One(GradientSpec),
1435 Many(Vec<GradientSpec>),
1436}
1437
1438#[derive(Debug, Clone, Deserialize)]
1443#[serde(untagged)]
1444pub enum ImageMode {
1445 Keyword(String),
1447 Spec(ImageModeSpec),
1448}
1449
1450#[derive(Debug, Clone, Deserialize)]
1452#[serde(tag = "type", rename_all = "camelCase")]
1453pub enum ImageModeSpec {
1454 Sliced(SliceSpec),
1455 Tiled(TiledSpec),
1456}
1457
1458#[derive(Debug, Clone, Default, Deserialize)]
1460#[serde(rename_all = "camelCase")]
1461pub struct SliceSpec {
1462 #[serde(default)]
1465 pub border: SliceBorder,
1466 #[serde(default)]
1468 pub center_scale_mode: Option<SliceScale>,
1469 #[serde(default)]
1471 pub sides_scale_mode: Option<SliceScale>,
1472 #[serde(default)]
1474 pub max_corner_scale: Option<f32>,
1475}
1476
1477#[derive(Debug, Clone, Default, Deserialize)]
1480#[serde(untagged)]
1481pub enum SliceBorder {
1482 #[default]
1484 Zero,
1485 Uniform(f32),
1487 Sides {
1489 #[serde(default)]
1490 top: f32,
1491 #[serde(default)]
1492 right: f32,
1493 #[serde(default)]
1494 bottom: f32,
1495 #[serde(default)]
1496 left: f32,
1497 },
1498}
1499
1500#[derive(Debug, Clone, Deserialize)]
1503#[serde(untagged)]
1504pub enum SliceScale {
1505 Keyword(String),
1506 Tile { tile: f32 },
1507}
1508
1509#[derive(Debug, Clone, Default, Deserialize)]
1511#[serde(rename_all = "camelCase")]
1512pub struct TiledSpec {
1513 #[serde(default)]
1514 pub tile_x: bool,
1515 #[serde(default)]
1516 pub tile_y: bool,
1517 #[serde(default)]
1519 pub stretch_value: Option<f32>,
1520}
1521
1522#[derive(Debug, Clone, Copy, Deserialize)]
1525#[serde(rename_all = "camelCase")]
1526pub struct SourceRect {
1527 pub x: f32,
1528 pub y: f32,
1529 pub width: f32,
1530 pub height: f32,
1531}
1532
1533#[derive(Debug, Clone, Deserialize)]
1538#[serde(rename_all = "camelCase")]
1539pub struct AtlasSpec {
1540 pub tile_width: u32,
1541 pub tile_height: u32,
1542 pub columns: u32,
1543 pub rows: u32,
1544 #[serde(default)]
1546 pub padding: Option<[u32; 2]>,
1547 #[serde(default)]
1549 pub offset: Option<[u32; 2]>,
1550 #[serde(default)]
1552 pub index: usize,
1553}
1554
1555#[derive(Debug, Clone, PartialEq)]
1571pub enum Animatable<T> {
1572 Static(T),
1573 Animated(crate::animations::protocol::Binding),
1574}
1575
1576impl<T> Animatable<T> {
1577 pub fn value(&self) -> Option<&T> {
1579 match self {
1580 Animatable::Static(v) => Some(v),
1581 Animatable::Animated(_) => None,
1582 }
1583 }
1584
1585 pub fn binding(&self) -> Option<&crate::animations::protocol::Binding> {
1587 match self {
1588 Animatable::Static(_) => None,
1589 Animatable::Animated(b) => Some(b),
1590 }
1591 }
1592}
1593
1594pub trait AnimatableField<T> {
1597 fn static_val(&self) -> Option<T>
1599 where
1600 T: Copy;
1601 fn static_ref(&self) -> Option<&T>;
1603 fn binding(&self) -> Option<&crate::animations::protocol::Binding>;
1605}
1606
1607impl<T> AnimatableField<T> for Option<Animatable<T>> {
1608 fn static_val(&self) -> Option<T>
1609 where
1610 T: Copy,
1611 {
1612 self.static_ref().copied()
1613 }
1614 fn static_ref(&self) -> Option<&T> {
1615 self.as_ref().and_then(Animatable::value)
1616 }
1617 fn binding(&self) -> Option<&crate::animations::protocol::Binding> {
1618 self.as_ref().and_then(Animatable::binding)
1619 }
1620}
1621
1622impl<'de, T: de::DeserializeOwned> Deserialize<'de> for Animatable<T> {
1623 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1624 let v = serde_json::Value::deserialize(d)?;
1625 if let Some(inner) = v.as_object().and_then(|m| m.get("animated")) {
1626 return Ok(Animatable::Animated(binding_from_wrapper(inner)));
1627 }
1628 T::deserialize(v)
1629 .map(Animatable::Static)
1630 .map_err(de::Error::custom)
1631 }
1632}
1633
1634pub(crate) fn binding_from_wrapper(
1639 inner: &serde_json::Value,
1640) -> crate::animations::protocol::Binding {
1641 use crate::animations::protocol::Binding;
1642 let inert = Binding::Shared { id: 0 };
1643 let Some(map) = inner.as_object() else {
1644 decode_warn(
1645 "styleBinding",
1646 &inner.to_string(),
1647 "animated must be a shared value or an interpolate/interpolateColor descriptor",
1648 );
1649 return inert;
1650 };
1651 if map.contains_key("type") {
1652 match Binding::deserialize(inner) {
1653 Ok(b) => b,
1654 Err(e) => {
1655 decode_warn("styleBinding", &inner.to_string(), &e.to_string());
1656 inert
1657 }
1658 }
1659 } else if let Some(id) = map.get("id").and_then(serde_json::Value::as_u64) {
1660 Binding::Shared { id: id as u32 }
1661 } else {
1662 decode_warn(
1663 "styleBinding",
1664 &inner.to_string(),
1665 "animated needs a shared value ({id}) or a descriptor ({type, id, …})",
1666 );
1667 inert
1668 }
1669}
1670
1671#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
1675#[serde(rename_all = "camelCase")]
1676pub struct Transform {
1677 pub translate_x: Option<Animatable<Length>>,
1680 pub translate_y: Option<Animatable<Length>>,
1683 pub scale: Option<Animatable<f32>>,
1685 pub scale_x: Option<Animatable<f32>>,
1686 pub scale_y: Option<Animatable<f32>>,
1687 pub rotate: Option<Animatable<Angle>>,
1690}
1691
1692#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
1698#[serde(rename_all = "camelCase")]
1699pub struct Transform3d {
1700 pub perspective: Option<Animatable<f32>>,
1704 pub translate_x: Option<Animatable<f32>>,
1706 pub translate_y: Option<Animatable<f32>>,
1708 pub translate_z: Option<Animatable<f32>>,
1711 pub rotate_x: Option<Animatable<Angle>>,
1713 pub rotate_y: Option<Animatable<Angle>>,
1715 pub rotate_z: Option<Animatable<Angle>>,
1717 pub scale: Option<Animatable<f32>>,
1719 pub scale_x: Option<Animatable<f32>>,
1720 pub scale_y: Option<Animatable<f32>>,
1721 pub origin: Option<Transform3dOrigin>,
1724}
1725
1726impl Transform3d {
1727 pub fn is_identity(&self) -> bool {
1731 let Self {
1732 perspective,
1733 translate_x,
1734 translate_y,
1735 translate_z,
1736 rotate_x,
1737 rotate_y,
1738 rotate_z,
1739 scale,
1740 scale_x,
1741 scale_y,
1742 origin: _, } = self;
1744 let no_binding = |f: &Option<Animatable<f32>>| f.binding().is_none();
1748 let no_angle_binding = |f: &Option<Animatable<Angle>>| f.binding().is_none();
1749 perspective.is_none()
1750 && [
1751 translate_x,
1752 translate_y,
1753 translate_z,
1754 scale,
1755 scale_x,
1756 scale_y,
1757 ]
1758 .iter()
1759 .all(|f| no_binding(f))
1760 && [rotate_x, rotate_y, rotate_z]
1761 .iter()
1762 .all(|f| no_angle_binding(f))
1763 && translate_x.static_val().unwrap_or(0.0) == 0.0
1764 && translate_y.static_val().unwrap_or(0.0) == 0.0
1765 && translate_z.static_val().unwrap_or(0.0) == 0.0
1766 && rotate_x.static_val().unwrap_or_default().radians() == 0.0
1767 && rotate_y.static_val().unwrap_or_default().radians() == 0.0
1768 && rotate_z.static_val().unwrap_or_default().radians() == 0.0
1769 && scale.static_val().unwrap_or(1.0) == 1.0
1770 && scale_x.static_val().unwrap_or(1.0) == 1.0
1771 && scale_y.static_val().unwrap_or(1.0) == 1.0
1772 }
1773}
1774
1775#[derive(Debug, Clone, PartialEq, Deserialize)]
1778pub struct Transform3dOrigin {
1779 pub x: Animatable<Length>,
1780 pub y: Animatable<Length>,
1781}
1782
1783impl Default for Transform3dOrigin {
1784 fn default() -> Self {
1785 Self {
1786 x: Animatable::Static(Length::Percent(50.0)),
1787 y: Animatable::Static(Length::Percent(50.0)),
1788 }
1789 }
1790}
1791
1792#[derive(Debug, Clone, Copy, PartialEq)]
1795pub enum Length {
1796 Auto,
1797 Px(f32),
1798 Percent(f32),
1799 Vw(f32),
1800 Vh(f32),
1801 VMin(f32),
1802 VMax(f32),
1803}
1804
1805impl Default for Length {
1806 fn default() -> Self {
1807 Length::Px(0.0)
1808 }
1809}
1810
1811fn parse_length(s: &str) -> Result<Length, String> {
1813 let s = s.trim();
1814 if s.eq_ignore_ascii_case("auto") {
1815 return Ok(Length::Auto);
1816 }
1817 type LengthCtor = fn(f32) -> Length;
1820 let units: [(&str, LengthCtor); 6] = [
1821 ("px", Length::Px),
1822 ("vmin", Length::VMin),
1823 ("vmax", Length::VMax),
1824 ("vw", Length::Vw),
1825 ("vh", Length::Vh),
1826 ("%", Length::Percent),
1827 ];
1828 for (suffix, ctor) in units {
1829 if let Some(num) = s.strip_suffix(suffix) {
1830 let v: f32 = num
1831 .trim()
1832 .parse()
1833 .map_err(|_| format!("invalid length {s:?}"))?;
1834 return Ok(ctor(v));
1835 }
1836 }
1837 s.parse::<f32>()
1838 .map(Length::Px)
1839 .map_err(|_| format!("invalid length {s:?}"))
1840}
1841
1842impl<'de> Deserialize<'de> for Length {
1843 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1844 struct LengthVisitor;
1845 impl<'de> Visitor<'de> for LengthVisitor {
1846 type Value = Length;
1847 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1848 f.write_str("a number (logical pixels) or a CSS length string")
1849 }
1850 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Length, E> {
1851 Ok(Length::Px(v as f32))
1852 }
1853 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Length, E> {
1854 Ok(Length::Px(v as f32))
1855 }
1856 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Length, E> {
1857 Ok(Length::Px(v as f32))
1858 }
1859 fn visit_str<E: de::Error>(self, s: &str) -> Result<Length, E> {
1860 Ok(parse_length(s).unwrap_or_else(|e| {
1861 decode_warn("length", s, &e);
1862 Length::default()
1863 }))
1864 }
1865 }
1866 d.deserialize_any(LengthVisitor)
1867 }
1868}
1869
1870#[derive(Debug, Clone, Copy, PartialEq, Default)]
1874pub struct Angle(f32);
1875
1876impl Angle {
1877 pub fn radians(self) -> f32 {
1879 self.0
1880 }
1881
1882 pub fn from_radians(radians: f32) -> Self {
1885 Angle(radians)
1886 }
1887}
1888
1889fn parse_angle(s: &str) -> Result<f32, String> {
1893 use std::f32::consts::{PI, TAU};
1894 let s = s.trim();
1895 type AngleConv = fn(f32) -> f32;
1896 let units: [(&str, AngleConv); 4] = [
1897 ("deg", f32::to_radians),
1898 ("grad", |v| v * PI / 200.0),
1899 ("turn", |v| v * TAU),
1900 ("rad", |v| v),
1901 ];
1902 for (suffix, conv) in units {
1903 if let Some(num) = s.strip_suffix(suffix) {
1904 let v: f32 = num
1905 .trim()
1906 .parse()
1907 .map_err(|_| format!("invalid angle {s:?}"))?;
1908 return Ok(conv(v));
1909 }
1910 }
1911 s.parse::<f32>()
1912 .map(f32::to_radians)
1913 .map_err(|_| format!("invalid angle {s:?}"))
1914}
1915
1916impl<'de> Deserialize<'de> for Angle {
1917 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1918 struct AngleVisitor;
1919 impl Visitor<'_> for AngleVisitor {
1920 type Value = Angle;
1921 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1922 f.write_str("a number (degrees) or a CSS angle string")
1923 }
1924 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Angle, E> {
1925 Ok(Angle((v as f32).to_radians()))
1926 }
1927 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Angle, E> {
1928 Ok(Angle((v as f32).to_radians()))
1929 }
1930 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Angle, E> {
1931 Ok(Angle((v as f32).to_radians()))
1932 }
1933 fn visit_str<E: de::Error>(self, s: &str) -> Result<Angle, E> {
1934 Ok(parse_angle(s).map(Angle).unwrap_or_else(|e| {
1935 decode_warn("angle", s, &e);
1936 Angle::default()
1937 }))
1938 }
1939 }
1940 d.deserialize_any(AngleVisitor)
1941 }
1942}
1943
1944#[derive(Debug, Clone, Copy, PartialEq, Default)]
1948pub struct Time(f32);
1949
1950impl Time {
1951 pub fn from_secs(secs: f32) -> Self {
1953 Time(secs)
1954 }
1955 pub fn seconds(self) -> f32 {
1957 self.0
1958 }
1959}
1960
1961fn parse_time(s: &str) -> Result<f32, String> {
1965 let s = s.trim();
1966 if let Some(num) = s.strip_suffix("ms") {
1967 return num
1968 .trim()
1969 .parse::<f32>()
1970 .map(|v| v / 1000.0)
1971 .map_err(|_| format!("invalid time {s:?}"));
1972 }
1973 if let Some(num) = s.strip_suffix('s') {
1974 return num
1975 .trim()
1976 .parse::<f32>()
1977 .map_err(|_| format!("invalid time {s:?}"));
1978 }
1979 s.parse::<f32>()
1980 .map(|v| v / 1000.0)
1981 .map_err(|_| format!("invalid time {s:?}"))
1982}
1983
1984impl<'de> Deserialize<'de> for Time {
1985 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1986 struct TimeVisitor;
1987 impl Visitor<'_> for TimeVisitor {
1988 type Value = Time;
1989 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1990 f.write_str("a number (milliseconds) or a CSS time string")
1991 }
1992 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Time, E> {
1993 Ok(Time(v as f32 / 1000.0))
1994 }
1995 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Time, E> {
1996 Ok(Time(v as f32 / 1000.0))
1997 }
1998 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Time, E> {
1999 Ok(Time(v as f32 / 1000.0))
2000 }
2001 fn visit_str<E: de::Error>(self, s: &str) -> Result<Time, E> {
2002 Ok(parse_time(s).map(Time).unwrap_or_else(|e| {
2003 decode_warn("time", s, &e);
2004 Time::default()
2005 }))
2006 }
2007 }
2008 d.deserialize_any(TimeVisitor)
2009 }
2010}
2011
2012#[derive(Debug, Clone, Copy, PartialEq)]
2017pub enum FontSize {
2018 Px(f32),
2019 Vw(f32),
2020 Vh(f32),
2021 VMin(f32),
2022 VMax(f32),
2023 Rem(f32),
2024}
2025
2026fn parse_font_size(s: &str) -> Result<FontSize, String> {
2030 let s = s.trim();
2031 type FsCtor = fn(f32) -> FontSize;
2032 let units: [(&str, FsCtor); 6] = [
2033 ("px", FontSize::Px),
2034 ("rem", FontSize::Rem),
2035 ("vmin", FontSize::VMin),
2036 ("vmax", FontSize::VMax),
2037 ("vw", FontSize::Vw),
2038 ("vh", FontSize::Vh),
2039 ];
2040 for (suffix, ctor) in units {
2041 if let Some(num) = s.strip_suffix(suffix) {
2042 let v: f32 = num
2043 .trim()
2044 .parse()
2045 .map_err(|_| format!("invalid fontSize {s:?}"))?;
2046 return Ok(ctor(v));
2047 }
2048 }
2049 s.parse::<f32>()
2050 .map(FontSize::Px)
2051 .map_err(|_| format!("invalid fontSize {s:?}"))
2052}
2053
2054impl<'de> Deserialize<'de> for FontSize {
2055 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2056 struct FontSizeVisitor;
2057 impl Visitor<'_> for FontSizeVisitor {
2058 type Value = FontSize;
2059 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2060 f.write_str("a number (logical pixels) or a font-size unit string")
2061 }
2062 fn visit_f64<E: de::Error>(self, v: f64) -> Result<FontSize, E> {
2063 Ok(FontSize::Px(v as f32))
2064 }
2065 fn visit_i64<E: de::Error>(self, v: i64) -> Result<FontSize, E> {
2066 Ok(FontSize::Px(v as f32))
2067 }
2068 fn visit_u64<E: de::Error>(self, v: u64) -> Result<FontSize, E> {
2069 Ok(FontSize::Px(v as f32))
2070 }
2071 fn visit_str<E: de::Error>(self, s: &str) -> Result<FontSize, E> {
2072 Ok(parse_font_size(s).unwrap_or_else(|e| {
2073 decode_warn("fontSize", s, &e);
2074 FontSize::Px(0.0)
2075 }))
2076 }
2077 }
2078 d.deserialize_any(FontSizeVisitor)
2079 }
2080}
2081
2082#[derive(Debug, Clone, Copy, PartialEq, Default)]
2085pub struct Rect {
2086 pub top: Length,
2087 pub right: Length,
2088 pub bottom: Length,
2089 pub left: Length,
2090}
2091
2092impl Rect {
2093 fn uniform(v: Length) -> Self {
2094 Rect {
2095 top: v,
2096 right: v,
2097 bottom: v,
2098 left: v,
2099 }
2100 }
2101
2102 fn from_shorthand(values: &[Length]) -> Result<Self, String> {
2104 Ok(match values {
2105 [a] => Rect::uniform(*a),
2106 [a, b] => Rect {
2107 top: *a,
2108 bottom: *a,
2109 right: *b,
2110 left: *b,
2111 },
2112 [a, b, c] => Rect {
2113 top: *a,
2114 right: *b,
2115 left: *b,
2116 bottom: *c,
2117 },
2118 [a, b, c, d] => Rect {
2119 top: *a,
2120 right: *b,
2121 bottom: *c,
2122 left: *d,
2123 },
2124 _ => return Err("expected 1–4 length values".into()),
2125 })
2126 }
2127}
2128
2129impl<'de> Deserialize<'de> for Rect {
2130 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2131 struct RectVisitor;
2132 impl<'de> Visitor<'de> for RectVisitor {
2133 type Value = Rect;
2134 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2135 f.write_str("a number, a CSS shorthand string, or a {top,right,bottom,left} object")
2136 }
2137 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Rect, E> {
2138 Ok(Rect::uniform(Length::Px(v as f32)))
2139 }
2140 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Rect, E> {
2141 Ok(Rect::uniform(Length::Px(v as f32)))
2142 }
2143 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Rect, E> {
2144 Ok(Rect::uniform(Length::Px(v as f32)))
2145 }
2146 fn visit_str<E: de::Error>(self, s: &str) -> Result<Rect, E> {
2147 let values: Vec<Length> = s
2150 .split_whitespace()
2151 .map(|tok| {
2152 parse_length(tok).unwrap_or_else(|e| {
2153 decode_warn("rect", tok, &e);
2154 Length::default()
2155 })
2156 })
2157 .collect();
2158 Ok(Rect::from_shorthand(&values).unwrap_or_else(|e| {
2159 decode_warn("rect", s, &format!("invalid rect {s:?}: {e}"));
2160 Rect::default()
2161 }))
2162 }
2163 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Rect, A::Error> {
2164 let mut rect = Rect::default();
2165 while let Some(key) = map.next_key::<String>()? {
2166 let v = map.next_value::<Length>()?;
2167 match key.as_str() {
2168 "top" => rect.top = v,
2169 "right" => rect.right = v,
2170 "bottom" => rect.bottom = v,
2171 "left" => rect.left = v,
2172 _ => decode_warn(
2175 "rect",
2176 &key,
2177 &format!(
2178 "unknown rect side {key:?}; ignoring (expected top/right/bottom/left)"
2179 ),
2180 ),
2181 }
2182 }
2183 Ok(rect)
2184 }
2185 }
2186 d.deserialize_any(RectVisitor)
2187 }
2188}
2189
2190macro_rules! keyword_fields {
2197 ( $(
2198 $(#[$meta:meta])*
2199 fn $fn_name:ident($kind:literal) -> $ty:ty {
2200 $( $($kw:literal)|+ => $variant:ident ),+ $(,)?
2201 }
2202 )+ ) => { $(
2203 $(#[$meta])*
2204 fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
2205 struct V;
2206 impl<'de> Visitor<'de> for V {
2207 type Value = Option<$ty>;
2208 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2209 f.write_str(concat!("a `", $kind, "` keyword string"))
2210 }
2211 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2212 Ok(Some(match s {
2213 $( $($kw)|+ => <$ty>::$variant, )+
2214 _ => {
2215 decode_warn(
2216 $kind,
2217 s,
2218 &format!("unrecognized {} {s:?}", $kind),
2219 );
2220 <$ty>::default()
2221 }
2222 }))
2223 }
2224 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
2225 Ok(None)
2226 }
2227 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
2228 Ok(None)
2229 }
2230 }
2231 d.deserialize_any(V)
2232 }
2233 )+ };
2234}
2235
2236keyword_fields! {
2237 fn de_display("display") -> Display {
2238 "flex" => Flex, "grid" => Grid, "block" => Block, "none" => None,
2239 }
2240 fn de_layer_cache("cache") -> LayerCache {
2241 "auto" => Auto, "always" => Always, "never" => Never,
2242 }
2243 fn de_box_sizing("boxSizing") -> BoxSizing {
2244 "borderBox" | "border-box" => BorderBox,
2245 "contentBox" | "content-box" => ContentBox,
2246 }
2247 fn de_position_type("positionType") -> PositionType {
2248 "absolute" => Absolute, "relative" => Relative,
2249 }
2250 fn de_overflow_axis("overflow") -> OverflowAxis {
2251 "visible" => Visible, "clip" => Clip, "hidden" => Hidden, "scroll" => Scroll,
2252 }
2253 fn de_align_items("alignItems") -> AlignItems {
2259 "start" => Start, "end" => End,
2260 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
2261 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
2262 }
2263 fn de_justify_items("justifyItems") -> JustifyItems {
2264 "start" => Start, "end" => End,
2265 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
2266 }
2267 fn de_align_self("alignSelf") -> AlignSelf {
2268 "auto" => Auto, "start" => Start, "end" => End,
2269 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
2270 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
2271 }
2272 fn de_justify_self("justifySelf") -> JustifySelf {
2273 "auto" => Auto, "start" => Start, "end" => End,
2274 "center" => Center, "baseline" => Baseline, "stretch" => Stretch,
2275 }
2276 fn de_align_content("alignContent") -> AlignContent {
2277 "start" => Start, "end" => End,
2278 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
2279 "center" => Center, "stretch" => Stretch,
2280 "spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
2281 "spaceAround" => SpaceAround,
2282 }
2283 fn de_justify_content("justifyContent") -> JustifyContent {
2284 "start" => Start, "end" => End,
2285 "flexStart" => FlexStart, "flexEnd" => FlexEnd,
2286 "center" => Center, "stretch" => Stretch,
2287 "spaceBetween" => SpaceBetween, "spaceEvenly" => SpaceEvenly,
2288 "spaceAround" => SpaceAround,
2289 }
2290 fn de_flex_direction("flexDirection") -> FlexDirection {
2291 "row" => Row, "column" => Column,
2292 "rowReverse" => RowReverse, "columnReverse" => ColumnReverse,
2293 }
2294 fn de_flex_wrap("flexWrap") -> FlexWrap {
2295 "nowrap" | "noWrap" => NoWrap, "wrap" => Wrap, "wrapReverse" => WrapReverse,
2296 }
2297 fn de_grid_auto_flow("gridAutoFlow") -> GridAutoFlow {
2298 "row" => Row, "column" => Column,
2299 "rowDense" => RowDense, "columnDense" => ColumnDense,
2300 }
2301 fn de_focus_policy("focusPolicy") -> FocusPolicy {
2304 "block" => Block, "pass" => Pass,
2305 }
2306 fn de_text_align("textAlign") -> Justify {
2307 "left" => Left, "center" => Center, "right" => Right,
2308 "justify" => Justified, "start" => Start, "end" => End,
2309 }
2310 fn de_line_break("lineBreak") -> LineBreak {
2311 "wordBoundary" => WordBoundary, "anyCharacter" => AnyCharacter,
2312 "wordOrCharacter" => WordOrCharacter, "noWrap" => NoWrap,
2313 }
2314}
2315
2316fn de_font_weight<'de, D: Deserializer<'de>>(d: D) -> Result<Option<FontWeight>, D::Error> {
2320 struct V;
2321 impl<'de> Visitor<'de> for V {
2322 type Value = Option<FontWeight>;
2323 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2324 f.write_str("a `fontWeight` keyword or numeric weight string")
2325 }
2326 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2327 Ok(Some(match s {
2328 "thin" => FontWeight::THIN,
2329 "light" => FontWeight(300),
2330 "normal" => FontWeight::NORMAL,
2331 "medium" => FontWeight(500),
2332 "semibold" => FontWeight(600),
2333 "bold" => FontWeight::BOLD,
2334 "black" => FontWeight::BLACK,
2335 other => other.parse::<u16>().map(FontWeight).unwrap_or_else(|_| {
2336 decode_warn(
2337 "fontWeight",
2338 other,
2339 &format!("unrecognized fontWeight {other:?}"),
2340 );
2341 FontWeight::NORMAL
2342 }),
2343 }))
2344 }
2345 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
2346 Ok(None)
2347 }
2348 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
2349 Ok(None)
2350 }
2351 }
2352 d.deserialize_any(V)
2353}
2354
2355fn split_tracks(s: &str) -> Vec<String> {
2358 let mut out = Vec::new();
2359 let mut depth = 0usize;
2360 let mut cur = String::new();
2361 for ch in s.chars() {
2362 match ch {
2363 '(' => {
2364 depth += 1;
2365 cur.push(ch);
2366 }
2367 ')' => {
2368 depth = depth.saturating_sub(1);
2369 cur.push(ch);
2370 }
2371 c if c.is_whitespace() && depth == 0 => {
2372 if !cur.is_empty() {
2373 out.push(std::mem::take(&mut cur));
2374 }
2375 }
2376 c => cur.push(c),
2377 }
2378 }
2379 if !cur.is_empty() {
2380 out.push(cur);
2381 }
2382 out
2383}
2384
2385fn single_track(token: &str) -> Option<GridTrack> {
2388 let t = token.trim();
2389 match t {
2390 "auto" => return Some(GridTrack::auto()),
2391 "min-content" => return Some(GridTrack::min_content()),
2392 "max-content" => return Some(GridTrack::max_content()),
2393 _ => {}
2394 }
2395 let parse = |num: &str| num.trim().parse::<f32>().ok();
2396 if let Some(v) = t.strip_suffix("fr").and_then(parse) {
2397 Some(GridTrack::fr(v))
2398 } else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
2399 Some(GridTrack::flex(v))
2400 } else if let Some(v) = t.strip_suffix("px").and_then(parse) {
2401 Some(GridTrack::px(v))
2402 } else {
2403 t.strip_suffix('%').and_then(parse).map(GridTrack::percent)
2404 }
2405}
2406
2407fn repeated_track(count: u16, token: &str) -> Option<RepeatedGridTrack> {
2409 let t = token.trim();
2410 match t {
2411 "auto" => return Some(RepeatedGridTrack::auto(count)),
2412 "min-content" => return Some(RepeatedGridTrack::min_content(count)),
2413 "max-content" => return Some(RepeatedGridTrack::max_content(count)),
2414 _ => {}
2415 }
2416 let parse = |num: &str| num.trim().parse::<f32>().ok();
2417 if let Some(v) = t.strip_suffix("fr").and_then(parse) {
2418 Some(RepeatedGridTrack::fr(count, v))
2419 } else if let Some(v) = t.strip_suffix("flex").and_then(parse) {
2420 Some(RepeatedGridTrack::flex(count, v))
2421 } else if let Some(v) = t.strip_suffix("px").and_then(parse) {
2422 Some(RepeatedGridTrack::px(count as usize, v))
2423 } else {
2424 t.strip_suffix('%')
2425 .and_then(parse)
2426 .map(|v| RepeatedGridTrack::percent(count as usize, v))
2427 }
2428}
2429
2430fn parse_template(s: &str) -> Vec<RepeatedGridTrack> {
2433 split_tracks(s)
2434 .into_iter()
2435 .filter_map(|tok| {
2436 let parse_one = || {
2437 if let Some(inner) = tok
2438 .strip_prefix("repeat(")
2439 .and_then(|t| t.strip_suffix(')'))
2440 {
2441 let (count, track) = inner.split_once(',')?;
2442 repeated_track(count.trim().parse().ok()?, track)
2443 } else {
2444 single_track(&tok).map(Into::into)
2445 }
2446 };
2447 let parsed = parse_one();
2448 if parsed.is_none() {
2449 decode_warn(
2450 "gridTrack",
2451 &tok,
2452 &format!("ignoring unparsable grid track {tok:?}"),
2453 );
2454 }
2455 parsed
2456 })
2457 .collect()
2458}
2459
2460fn parse_auto_tracks(s: &str) -> Vec<GridTrack> {
2462 split_tracks(s)
2463 .iter()
2464 .filter_map(|t| {
2465 let parsed = single_track(t);
2466 if parsed.is_none() {
2467 decode_warn(
2468 "gridTrack",
2469 t,
2470 &format!("ignoring unparsable grid track {t:?}"),
2471 );
2472 }
2473 parsed
2474 })
2475 .collect()
2476}
2477
2478fn try_grid_placement(s: &str) -> Option<GridPlacement> {
2483 enum Token {
2484 Num(i16), Span(u16), Auto,
2487 Invalid, }
2489 fn token(t: &str) -> Token {
2490 let t = t.trim();
2491 if t == "auto" {
2492 return Token::Auto;
2493 }
2494 if let Some(n) = t.strip_prefix("span") {
2495 return match n.trim().parse::<u16>() {
2496 Ok(0) | Err(_) => Token::Invalid,
2497 Ok(n) => Token::Span(n),
2498 };
2499 }
2500 match t.parse::<i16>() {
2501 Ok(0) | Err(_) => Token::Invalid,
2502 Ok(n) => Token::Num(n),
2503 }
2504 }
2505 use Token::*;
2506 if let Some((a, b)) = s.split_once('/') {
2507 return Some(match (token(a), token(b)) {
2508 (Num(start), Span(span)) => GridPlacement::start_span(start, span),
2509 (Auto, Span(span)) => GridPlacement::span(span),
2510 (Num(start), Num(end)) => GridPlacement::start_end(start, end),
2511 (Num(start), Auto) => GridPlacement::start(start),
2512 (Auto, Num(end)) => GridPlacement::end(end),
2513 (Auto, Auto) => GridPlacement::auto(),
2514 _ => return None,
2515 });
2516 }
2517 match token(s) {
2518 Auto => Some(GridPlacement::auto()),
2519 Span(span) => Some(GridPlacement::span(span)),
2520 Num(line) => Some(GridPlacement::start(line)),
2521 Invalid => None,
2522 }
2523}
2524
2525macro_rules! grid_fields {
2528 ( $(
2529 $(#[$meta:meta])*
2530 fn $fn_name:ident($expect:literal) -> $ty:ty { $parse:expr }
2531 )+ ) => { $(
2532 $(#[$meta])*
2533 fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$ty>, D::Error> {
2534 struct V;
2535 impl<'de> Visitor<'de> for V {
2536 type Value = Option<$ty>;
2537 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2538 f.write_str($expect)
2539 }
2540 fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
2541 let parse: fn(&str) -> $ty = $parse;
2542 Ok(Some(parse(s)))
2543 }
2544 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
2545 Ok(None)
2546 }
2547 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
2548 Ok(None)
2549 }
2550 }
2551 d.deserialize_any(V)
2552 }
2553 )+ };
2554}
2555
2556grid_fields! {
2557 fn de_grid_template("a CSS grid template string") -> Vec<RepeatedGridTrack> {
2558 parse_template
2559 }
2560 fn de_grid_auto_tracks("a grid auto-track list string") -> Vec<GridTrack> {
2561 parse_auto_tracks
2562 }
2563 fn de_grid_placement("a grid line placement string") -> GridPlacement {
2567 |s| {
2568 try_grid_placement(s).unwrap_or_else(|| {
2569 decode_warn(
2570 "gridPlacement",
2571 s,
2572 &format!("unrecognized grid placement {s:?}"),
2573 );
2574 GridPlacement::default()
2575 })
2576 }
2577 }
2578}
2579
2580#[derive(Debug, Clone, PartialEq, Default)]
2588pub struct BorderColorSpec {
2589 pub top: Option<String>,
2590 pub right: Option<String>,
2591 pub bottom: Option<String>,
2592 pub left: Option<String>,
2593}
2594
2595impl BorderColorSpec {
2596 fn uniform(s: String) -> Self {
2598 BorderColorSpec {
2599 top: Some(s.clone()),
2600 right: Some(s.clone()),
2601 bottom: Some(s.clone()),
2602 left: Some(s),
2603 }
2604 }
2605}
2606
2607impl<'de> Deserialize<'de> for BorderColorSpec {
2608 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2609 struct BorderColorVisitor;
2610 impl<'de> Visitor<'de> for BorderColorVisitor {
2611 type Value = BorderColorSpec;
2612 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
2613 f.write_str("a CSS color string or a {top,right,bottom,left} object of colors")
2614 }
2615 fn visit_str<E: de::Error>(self, s: &str) -> Result<BorderColorSpec, E> {
2616 Ok(BorderColorSpec::uniform(s.to_owned()))
2617 }
2618 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<BorderColorSpec, A::Error> {
2619 let mut spec = BorderColorSpec::default();
2620 while let Some(key) = map.next_key::<String>()? {
2621 let v = map.next_value::<String>()?;
2622 match key.as_str() {
2623 "top" => spec.top = Some(v),
2624 "right" => spec.right = Some(v),
2625 "bottom" => spec.bottom = Some(v),
2626 "left" => spec.left = Some(v),
2627 _ => decode_warn(
2630 "borderColor",
2631 &key,
2632 &format!(
2633 "unknown borderColor side {key:?}; ignoring (expected top/right/bottom/left)"
2634 ),
2635 ),
2636 }
2637 }
2638 Ok(spec)
2639 }
2640 }
2641 d.deserialize_any(BorderColorVisitor)
2642 }
2643}
2644
2645#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2648#[serde(rename_all = "camelCase")]
2649pub struct UiEvent {
2650 pub id: NodeId,
2651 pub kind: String,
2656 #[serde(default, skip_serializing_if = "Option::is_none")]
2659 pub x: Option<f32>,
2660 #[serde(default, skip_serializing_if = "Option::is_none")]
2663 pub y: Option<f32>,
2664 #[serde(default, skip_serializing_if = "Option::is_none")]
2668 pub client_x: Option<f32>,
2669 #[serde(default, skip_serializing_if = "Option::is_none")]
2672 pub client_y: Option<f32>,
2673 #[serde(default, skip_serializing_if = "Option::is_none")]
2678 pub button: Option<u8>,
2679 #[serde(default, skip_serializing_if = "Option::is_none")]
2681 pub value: Option<String>,
2682 #[serde(default, skip_serializing_if = "Option::is_none")]
2684 pub selection_start: Option<usize>,
2685 #[serde(default, skip_serializing_if = "Option::is_none")]
2687 pub selection_end: Option<usize>,
2688 #[serde(default, skip_serializing_if = "Option::is_none")]
2691 pub selection_direction: Option<String>,
2692 #[serde(default, skip_serializing_if = "Option::is_none")]
2695 pub composing: Option<bool>,
2696 #[serde(default, skip_serializing_if = "Option::is_none")]
2699 pub scroll_top: Option<f32>,
2700 #[serde(default, skip_serializing_if = "Option::is_none")]
2703 pub scroll_left: Option<f32>,
2704 #[serde(default, skip_serializing_if = "Option::is_none")]
2707 pub delta_x: Option<f32>,
2708 #[serde(default, skip_serializing_if = "Option::is_none")]
2711 pub delta_y: Option<f32>,
2712 #[serde(default, skip_serializing_if = "Option::is_none")]
2716 pub delta_mode: Option<String>,
2717 #[serde(default, skip_serializing_if = "Option::is_none")]
2722 pub width: Option<f32>,
2723 #[serde(default, skip_serializing_if = "Option::is_none")]
2726 pub height: Option<f32>,
2727}
2728
2729#[derive(Debug, Clone, Serialize)]
2733#[serde(tag = "t", rename_all = "camelCase")]
2734pub enum Outbound {
2735 UiEvent { event: UiEvent },
2737 Event {
2740 name: String,
2741 value: serde_json::Value,
2742 },
2743 Response { id: u64, result: ResponseResult },
2745 AnimationFinished {
2749 id: crate::animations::SharedId,
2750 token: u64,
2751 finished: bool,
2752 },
2753 Reload,
2755}
2756
2757#[derive(Debug, Clone, Serialize)]
2761#[serde(tag = "status", rename_all = "camelCase")]
2762pub enum ResponseResult {
2763 Ok { value: serde_json::Value },
2764 Err { message: String },
2765}
2766
2767#[cfg(test)]
2768mod tests {
2769 use super::*;
2770
2771 #[cfg(all(feature = "devtools", debug_assertions))]
2776 #[test]
2777 fn op_batch_attributes_decode_warnings() {
2778 crate::diag::decode_report("length", "stale", "stale entry");
2780 let json = r#"[
2781 {"op":"update","id":7,"props":{"style":{"width":"aa16"}}},
2782 {"op":"append","parent":0,"child":7},
2783 {"op":"update","id":9,"props":{"style":{"display":"flexx","padding":"1px bogus"}}}
2784 ]"#;
2785 let batch: OpBatch = serde_json::from_str(json).expect("batch decodes");
2786 assert_eq!(batch.0.len(), 3, "fallbacks must not drop ops");
2787 let warns = crate::diag::take_decode_warnings();
2788 let brief: Vec<_> = warns
2789 .iter()
2790 .map(|w| (w.node, w.kind, w.value.as_str()))
2791 .collect();
2792 assert_eq!(
2793 brief,
2794 vec![
2795 (Some(7), "length", "aa16"),
2796 (Some(9), "display", "flexx"),
2797 (Some(9), "rect", "bogus"),
2798 ],
2799 );
2800 assert!(warns.iter().all(|w| !w.message.is_empty()));
2801 assert!(
2802 crate::diag::take_decode_warnings().is_empty(),
2803 "drain empties the sink"
2804 );
2805 }
2806
2807 #[test]
2809 fn deserializes_editable_text_create() {
2810 let json = r#"{"op":"create","id":7,"kind":"editableText","props":{
2811 "value":"hi","maxLength":40,"multiline":true,"onChange":true,
2812 "autofocus":true,"selectionStart":0,"selectionEnd":2,
2813 "ariaLabel":"Name","onSelect":true,"onFocus":true,"onBlur":true,
2814 "focusStyle":{"borderColor":"white"}}}"#;
2815 match serde_json::from_str::<Op>(json).expect("valid op") {
2816 Op::Create {
2817 id, kind, props, ..
2818 } => {
2819 assert_eq!(id, 7);
2820 assert_eq!(kind, "editableText");
2821 assert_eq!(props.value.as_deref(), Some("hi"));
2822 assert_eq!(props.max_length, Some(40));
2823 assert!(props.multiline);
2824 assert!(props.on_change);
2825 assert!(props.autofocus);
2826 assert_eq!(props.selection_start, Some(0));
2827 assert_eq!(props.selection_end, Some(2));
2828 assert_eq!(props.aria_label.as_deref(), Some("Name"));
2829 assert!(props.on_select);
2830 assert!(props.on_focus);
2831 assert!(props.on_blur);
2832 assert!(props.focus_style.is_some());
2833 }
2834 other => panic!("expected create, got {other:?}"),
2835 }
2836 }
2837
2838 #[test]
2841 fn deserializes_transform_opacity_and_transition() {
2842 let s: Style = serde_json::from_str(
2843 r#"{
2844 "transform": { "scale": 0.95, "translateX": 4, "translateY": "50%" },
2845 "opacity": 0.5,
2846 "transition": { "transform": { "duration": 0.15, "easing": "easeOut" } }
2847 }"#,
2848 )
2849 .expect("style decodes");
2850 let t = s.transform.expect("transform present");
2851 assert_eq!(t.scale.static_val(), Some(0.95));
2852 assert_eq!(t.translate_x.static_val(), Some(Length::Px(4.0)));
2854 assert_eq!(t.translate_y.static_val(), Some(Length::Percent(50.0)));
2855 assert_eq!(t.scale_x, None);
2856 assert_eq!(s.opacity.static_val(), Some(0.5));
2857 let transition = s.transition.expect("transition present");
2858 assert!(transition.for_transform().is_some());
2859 assert!(transition.for_opacity().is_none());
2860 }
2861
2862 #[test]
2867 fn deserializes_transform3d() {
2868 let s: Style = serde_json::from_str(
2869 r#"{
2870 "transform3d": {
2871 "perspective": 800,
2872 "translateZ": -20,
2873 "rotateY": 45,
2874 "rotateX": "1.5rad",
2875 "rotateZ": "not-an-angle",
2876 "scale": 1.25,
2877 "origin": { "x": "50%", "y": 10 }
2878 }
2879 }"#,
2880 )
2881 .expect("style decodes");
2882 let t = s.transform3d.clone().expect("transform3d present");
2883 assert_eq!(t.perspective.static_val(), Some(800.0));
2884 assert_eq!(t.translate_z.static_val(), Some(-20.0));
2885 assert_eq!(
2886 t.rotate_y.static_val().unwrap().radians(),
2887 45f32.to_radians()
2888 );
2889 assert_eq!(t.rotate_x.static_val().unwrap().radians(), 1.5);
2890 assert_eq!(t.rotate_z.static_val().unwrap().radians(), 0.0);
2892 assert_eq!(t.scale.static_val(), Some(1.25));
2893 let origin = t.origin.clone().expect("origin present");
2894 assert_eq!(origin.x.value(), Some(&Length::Percent(50.0)));
2895 assert_eq!(origin.y.value(), Some(&Length::Px(10.0)));
2896 assert!(!t.is_identity());
2897
2898 let s: Style = serde_json::from_str(r#"{ "transform3d": {} }"#).expect("style decodes");
2899 assert!(s.transform3d.expect("present").is_identity());
2900
2901 let mut cached = Props::default();
2902 let (dirty, _) = cached.merge_delta(
2903 props(serde_json::json!({ "style": { "transform3d": { "rotateY": 45 } } })),
2904 &[],
2905 &[],
2906 );
2907 assert!(dirty.style.intersects(style_groups::TRANSFORM3D));
2908 assert!(dirty.style.intersects(style_groups::LAYER));
2909 assert!(dirty.style.intersects(style_groups::TRANSITION));
2910 }
2911
2912 #[test]
2916 fn group_alpha_decodes_and_dirties_layer() {
2917 let s: Style = serde_json::from_str(r#"{ "groupAlpha": false }"#).expect("style decodes");
2918 assert_eq!(s.group_alpha, Some(false));
2919 let s: Style = serde_json::from_str("{}").expect("style decodes");
2920 assert_eq!(s.group_alpha, None);
2921
2922 let mut cached = Props::default();
2924 let (dirty, _) = cached.merge_delta(
2925 props(serde_json::json!({ "style": { "groupAlpha": false } })),
2926 &[],
2927 &[],
2928 );
2929 assert!(dirty.style.intersects(style_groups::LAYER));
2930 let (dirty, _) = cached.merge_delta(
2931 props(serde_json::json!({ "style": { "opacity": 0.5 } })),
2932 &[],
2933 &[],
2934 );
2935 assert!(dirty.style.intersects(style_groups::LAYER));
2936 let style = cached.style.as_ref().expect("style retained");
2937 assert_eq!(style.group_alpha, Some(false));
2938 assert_eq!(style.opacity.static_val(), Some(0.5));
2939 }
2940
2941 #[test]
2944 fn cache_keyword_decodes_and_dirties_layer() {
2945 let s: Style = serde_json::from_str(r#"{ "cache": "always" }"#).expect("style decodes");
2946 assert_eq!(s.cache, Some(LayerCache::Always));
2947 let s: Style = serde_json::from_str(r#"{ "cache": "auto" }"#).expect("style decodes");
2948 assert_eq!(s.cache, Some(LayerCache::Auto));
2949 let s: Style = serde_json::from_str(r#"{ "cache": "never" }"#).expect("style decodes");
2950 assert_eq!(s.cache, Some(LayerCache::Never));
2951 let s: Style = serde_json::from_str("{}").expect("style decodes");
2952 assert_eq!(s.cache, None);
2953 let s: Style = serde_json::from_str(r#"{ "cache": "sometimes" }"#).expect("style decodes");
2955 assert_eq!(s.cache, Some(LayerCache::Auto));
2956
2957 let mut cached = Props::default();
2958 let (dirty, _) = cached.merge_delta(
2959 props(serde_json::json!({ "style": { "cache": "always" } })),
2960 &[],
2961 &[],
2962 );
2963 assert!(dirty.style.intersects(style_groups::LAYER));
2964 assert_eq!(
2965 cached.style.as_ref().and_then(|s| s.cache),
2966 Some(LayerCache::Always)
2967 );
2968 }
2969
2970 #[test]
2973 fn angle_units() {
2974 use std::f32::consts::{PI, TAU};
2975 let parse = |v: serde_json::Value| serde_json::from_value::<Angle>(v).unwrap().radians();
2976 assert!((parse(serde_json::json!(180)) - PI).abs() < 1e-5);
2977 assert!((parse(serde_json::json!("180deg")) - PI).abs() < 1e-5);
2978 assert!((parse(serde_json::json!("3.14159rad")) - PI).abs() < 1e-4);
2979 assert!((parse(serde_json::json!("0.5turn")) - PI).abs() < 1e-5);
2980 assert!((parse(serde_json::json!("400grad")) - TAU).abs() < 1e-5);
2981 }
2982
2983 #[test]
2986 fn border_color_scalar_and_per_side() {
2987 let uniform: Style =
2989 serde_json::from_str(r#"{ "borderColor": "white" }"#).expect("scalar decodes");
2990 let bc = uniform
2991 .border_color
2992 .static_ref()
2993 .expect("border_color present");
2994 assert_eq!(bc.top.as_deref(), Some("white"));
2995 assert_eq!(bc.right.as_deref(), Some("white"));
2996 assert_eq!(bc.bottom.as_deref(), Some("white"));
2997 assert_eq!(bc.left.as_deref(), Some("white"));
2998
2999 let sided: Style =
3001 serde_json::from_str(r##"{ "borderColor": { "top": "#f00", "left": "blue" } }"##)
3002 .expect("object decodes");
3003 let bc = sided
3004 .border_color
3005 .static_ref()
3006 .expect("border_color present");
3007 assert_eq!(bc.top.as_deref(), Some("#f00"));
3008 assert_eq!(bc.left.as_deref(), Some("blue"));
3009 assert_eq!(bc.right, None);
3010 assert_eq!(bc.bottom, None);
3011
3012 let bogus: Style =
3016 serde_json::from_str(r#"{ "borderColor": { "middle": "red", "top": "blue" } }"#)
3017 .expect("unknown side key must not abort deserialization");
3018 let bc = bogus
3019 .border_color
3020 .static_ref()
3021 .expect("border_color present");
3022 assert_eq!(bc.top.as_deref(), Some("blue"));
3023 assert_eq!(bc.right, None);
3024 assert_eq!(bc.bottom, None);
3025 assert_eq!(bc.left, None);
3026 }
3027
3028 #[test]
3032 fn bad_unit_values_fall_back_instead_of_aborting() {
3033 let s: Style = serde_json::from_str(r#"{ "width": "100pixels", "height": "40px" }"#)
3035 .expect("a bad length must not abort deserialization");
3036 assert_eq!(s.width.static_val(), Some(Length::default()));
3037 assert_eq!(s.height.static_val(), Some(Length::Px(40.0)));
3038
3039 let s: Style = serde_json::from_str(r#"{ "fontSize": "16pxx" }"#)
3041 .expect("bad fontSize must not abort");
3042 assert_eq!(s.font_size, Some(FontSize::Px(0.0)));
3043
3044 let t: Transform = serde_json::from_str(r#"{ "rotate": "45degg", "translateX": "50%" }"#)
3046 .expect("bad angle must not abort");
3047 assert_eq!(t.rotate.static_val(), Some(Angle::default()));
3048 assert_eq!(t.translate_x.static_val(), Some(Length::Percent(50.0)));
3049
3050 let s: Style =
3054 serde_json::from_str(r#"{ "padding": "16asd" }"#).expect("bad rect must not abort");
3055 assert_eq!(s.padding, Some(Rect::default()));
3056
3057 let s: Style = serde_json::from_str(r#"{ "padding": "8px 16asd" }"#)
3058 .expect("partial-bad rect must not abort");
3059 assert_eq!(
3061 s.padding,
3062 Some(Rect {
3063 top: Length::Px(8.0),
3064 bottom: Length::Px(8.0),
3065 right: Length::default(),
3066 left: Length::default(),
3067 })
3068 );
3069
3070 let s: Style = serde_json::from_str(r#"{ "padding": "8px 16px" }"#)
3071 .expect("valid two-value shorthand decodes");
3072 assert_eq!(
3073 s.padding,
3074 Some(Rect {
3075 top: Length::Px(8.0),
3076 bottom: Length::Px(8.0),
3077 right: Length::Px(16.0),
3078 left: Length::Px(16.0),
3079 })
3080 );
3081
3082 let s: Style = serde_json::from_str(r#"{ "padding": "1px 2px 3px 4px 5px" }"#)
3084 .expect("bad value-count must not abort");
3085 assert_eq!(s.padding, Some(Rect::default()));
3086 }
3087
3088 #[test]
3094 fn keyword_fields_decode_to_bevy_enums() {
3095 let s: Style = serde_json::from_value(serde_json::json!({
3096 "display": "grid",
3097 "alignItems": "start",
3098 "alignSelf": "flexStart",
3099 "alignContent": "spaceBetween",
3100 "justifyContent": "flexEnd",
3101 "flexWrap": "nowrap",
3102 "focusPolicy": "block",
3103 "textAlign": "justify",
3104 "lineBreak": "anyCharacter",
3105 }))
3106 .expect("keyword style decodes");
3107 assert_eq!(s.display, Some(Display::Grid));
3108 assert_eq!(s.align_items, Some(AlignItems::Start));
3109 assert_eq!(s.align_self, Some(AlignSelf::FlexStart));
3110 assert_eq!(s.align_content, Some(AlignContent::SpaceBetween));
3111 assert_eq!(s.justify_content, Some(JustifyContent::FlexEnd));
3112 assert_eq!(s.flex_wrap, Some(FlexWrap::NoWrap));
3113 assert_eq!(s.focus_policy, Some(FocusPolicy::Block));
3114 assert_eq!(s.text_align, Some(Justify::Justified));
3115 assert_eq!(s.line_break, Some(LineBreak::AnyCharacter));
3116
3117 let s: Style = serde_json::from_value(serde_json::json!({
3118 "alignItems": "flexStart",
3119 "justifyContent": "start",
3120 "boxSizing": "border-box",
3122 "flexWrap": "noWrap",
3123 }))
3124 .expect("alias keywords decode");
3125 assert_eq!(s.align_items, Some(AlignItems::FlexStart));
3126 assert_eq!(s.justify_content, Some(JustifyContent::Start));
3127 assert_eq!(s.box_sizing, Some(BoxSizing::BorderBox));
3128 assert_eq!(s.flex_wrap, Some(FlexWrap::NoWrap));
3129 }
3130
3131 #[test]
3135 fn unknown_enum_keywords_fall_back_to_default() {
3136 let s: Style = serde_json::from_value(serde_json::json!({
3137 "display": "flx",
3138 "alignItems": "centre",
3139 "flexDirection": "sideways",
3140 "textAlign": "middle",
3141 "fontWeight": "heavyish",
3142 "focusPolicy": "weird",
3143 "lineBreak": "wordBoundary",
3145 }))
3146 .expect("bad keywords must not abort deserialization");
3147 assert_eq!(s.display, Some(Display::default()));
3148 assert_eq!(s.align_items, Some(AlignItems::default()));
3149 assert_eq!(s.flex_direction, Some(FlexDirection::default()));
3150 assert_eq!(s.text_align, Some(Justify::default()));
3151 assert_eq!(s.font_weight, Some(FontWeight::NORMAL));
3152 assert_eq!(s.focus_policy, Some(FocusPolicy::Pass));
3153 assert_eq!(s.line_break, Some(LineBreak::WordBoundary));
3154 }
3155
3156 #[test]
3158 fn font_weight_keywords_and_numeric() {
3159 let fw = |v: serde_json::Value| {
3160 serde_json::from_value::<Style>(serde_json::json!({ "fontWeight": v }))
3161 .expect("fontWeight decodes")
3162 .font_weight
3163 };
3164 assert_eq!(fw("bold".into()), Some(FontWeight::BOLD));
3165 assert_eq!(fw("600".into()), Some(FontWeight(600)));
3166 assert_eq!(fw("thin".into()), Some(FontWeight::THIN));
3167 }
3168
3169 #[test]
3171 fn grid_templates_and_placement_decode() {
3172 let s: Style = serde_json::from_value(serde_json::json!({
3173 "gridTemplateColumns": "1fr 2fr 100px",
3174 "gridTemplateRows": "repeat(3, 1fr)",
3175 "gridAutoRows": "auto 40px",
3176 }))
3177 .expect("grid template decodes");
3178 assert_eq!(s.grid_template_columns.map(|t| t.len()), Some(3));
3179 assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(1));
3180 assert_eq!(s.grid_auto_rows.map(|t| t.len()), Some(2));
3181
3182 let s: Style =
3184 serde_json::from_value(serde_json::json!({ "gridTemplateRows": "1fr bogus 2fr" }))
3185 .expect("bad track must not abort");
3186 assert_eq!(s.grid_template_rows.map(|t| t.len()), Some(2));
3187
3188 let placed = |v: &str| {
3189 let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
3190 .expect("grid placement decodes");
3191 format!("{:?}", s.grid_row.unwrap())
3192 };
3193 let expect = |p: GridPlacement| format!("{p:?}");
3194 assert_eq!(placed("1 / 3"), expect(GridPlacement::start_end(1, 3)));
3195 assert_eq!(placed("span 2"), expect(GridPlacement::span(2)));
3196 assert_eq!(
3197 placed("2 / span 3"),
3198 expect(GridPlacement::start_span(2, 3))
3199 );
3200 assert_eq!(placed("2 / 2"), expect(GridPlacement::start_end(2, 2)));
3201 assert_eq!(placed("-1"), expect(GridPlacement::start(-1)));
3202 assert_eq!(placed("2 / auto"), expect(GridPlacement::start(2)));
3203 assert_eq!(placed("auto / 3"), expect(GridPlacement::end(3)));
3204 }
3205
3206 #[test]
3210 fn grid_placement_zero_falls_back_to_auto() {
3211 let placed = |v: &str| {
3212 let s: Style = serde_json::from_value(serde_json::json!({ "gridRow": v }))
3213 .expect("zero placement must not abort");
3214 format!("{:?}", s.grid_row.unwrap())
3215 };
3216 let auto = format!("{:?}", GridPlacement::auto());
3217 for s in ["0", "span 0", "0 / 2", "2 / 0", "0 / span 2", "2 / span 0"] {
3218 assert_eq!(placed(s), auto, "input {s:?}");
3219 }
3220 assert_eq!(placed("garbage"), auto);
3222 }
3223
3224 #[test]
3230 fn deserializes_filter_chain() {
3231 use crate::filters::FilterChain;
3232
3233 let s: Style =
3235 serde_json::from_str(r#"{ "filter": { "name": "blur", "params": { "radius": 4 } } }"#)
3236 .expect("filter decodes");
3237 let chain = s.filter.expect("filter present");
3238 assert_eq!(chain.0.len(), 1);
3239 assert_eq!(chain.0[0].name, "blur");
3240 assert_eq!(chain.0[0].params["radius"], serde_json::json!(4));
3241
3242 let s: Style =
3244 serde_json::from_str(r#"{ "filter": [{ "name": "blur" }, { "name": "grayscale" }] }"#)
3245 .expect("filter decodes");
3246 let names: Vec<&str> = s
3247 .filter
3248 .as_ref()
3249 .expect("filter present")
3250 .0
3251 .iter()
3252 .map(|u| u.name.as_str())
3253 .collect();
3254 assert_eq!(names, ["blur", "grayscale"]);
3255
3256 let s: Style =
3259 serde_json::from_str(r#"{ "filter": [{ "name": "blur" }, 3], "opacity": 0.5 }"#)
3260 .expect("a bad filter entry must not abort the style");
3261 assert_eq!(s.filter, Some(FilterChain::default()));
3262 assert_eq!(s.opacity.static_val(), Some(0.5));
3263 }
3264
3265 #[test]
3270 fn filter_delta_dirties_filter_and_layer() {
3271 let mut cached = Props::default();
3272 let (dirty, _) = cached.merge_delta(
3273 props(serde_json::json!({ "style": { "filter": { "name": "blur" } } })),
3274 &[],
3275 &[],
3276 );
3277 assert!(dirty.style.intersects(style_groups::FILTER));
3278 assert!(dirty.style.intersects(style_groups::LAYER));
3279
3280 let (dirty, _) = cached.merge_delta(
3281 props(serde_json::json!({ "hoverStyle": { "filter": { "name": "blur" } } })),
3282 &[],
3283 &[],
3284 );
3285 assert!(dirty.hover_style);
3286 let hover = cached.hover_style.as_ref().expect("variant retained");
3287 assert!(hover.filter.is_some(), "variant carries the chain");
3288 }
3289
3290 #[test]
3295 fn backdrop_filter_delta_dirties_backdrop_and_layer() {
3296 let mut cached = Props::default();
3297 let (dirty, _) = cached.merge_delta(
3298 props(serde_json::json!({ "style": { "backdropFilter": { "name": "blur" } } })),
3299 &[],
3300 &[],
3301 );
3302 assert!(dirty.style.intersects(style_groups::BACKDROP));
3303 assert!(dirty.style.intersects(style_groups::LAYER));
3304 assert!(!dirty.style.intersects(style_groups::FILTER));
3305 assert!(
3306 cached
3307 .style
3308 .as_ref()
3309 .is_some_and(|s| s.backdrop_filter.is_some())
3310 );
3311
3312 let (dirty, _) = cached.merge_delta(Props::default(), &[], &["backdropFilter".into()]);
3313 assert!(dirty.style.intersects(style_groups::BACKDROP));
3314 assert!(dirty.style.intersects(style_groups::LAYER));
3315 assert!(
3316 cached
3317 .style
3318 .as_ref()
3319 .is_some_and(|s| s.backdrop_filter.is_none())
3320 );
3321 }
3322
3323 #[test]
3326 fn serializes_change_event_with_value() {
3327 let ev = UiEvent {
3328 id: 7,
3329 kind: "change".into(),
3330 value: Some("hello".into()),
3331 ..Default::default()
3332 };
3333 let v = serde_json::to_value(&ev).expect("serializable");
3334 assert_eq!(v["kind"], "change");
3335 assert_eq!(v["value"], "hello");
3336 assert!(v.get("clientX").is_none(), "pointer fields omitted");
3337 assert!(v.get("button").is_none(), "button omitted on text events");
3338 }
3339
3340 #[test]
3343 fn serializes_pointer_event_with_button() {
3344 let ev = UiEvent {
3345 id: 3,
3346 kind: "pointerDown".into(),
3347 button: Some(2),
3348 ..Default::default()
3349 };
3350 let v = serde_json::to_value(&ev).expect("serializable");
3351 assert_eq!(v["kind"], "pointerDown");
3352 assert_eq!(v["button"], 2);
3353 }
3354
3355 #[test]
3359 fn style_field_table_is_complete() {
3360 macro_rules! build_full {
3361 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
3362 Style { $($f: None,)* }
3363 };
3364 }
3365 let _style: Style = with_style_fields!(build_full);
3366 }
3367
3368 #[test]
3372 fn style_wire_names_match_serde_rename() {
3373 fn camel(s: &str) -> String {
3374 let mut out = String::new();
3375 let mut up = false;
3376 for c in s.chars() {
3377 if c == '_' {
3378 up = true;
3379 } else if up {
3380 out.extend(c.to_uppercase());
3381 up = false;
3382 } else {
3383 out.push(c);
3384 }
3385 }
3386 out
3387 }
3388 macro_rules! check {
3389 ($(($f:ident, $name:literal, $g:tt, $ov:ident),)*) => {
3390 $( assert_eq!(camel(stringify!($f)), $name, "table wire name for `{}`", stringify!($f)); )*
3391 };
3392 }
3393 with_style_fields!(check);
3394 }
3395
3396 fn props(json: serde_json::Value) -> Props {
3397 serde_json::from_value(json).expect("valid props")
3398 }
3399
3400 #[test]
3402 fn merge_delta_sets_and_preserves() {
3403 let mut cached = props(serde_json::json!({
3404 "style": { "backgroundColor": "red", "outline": { "color": "white" } },
3405 "hoverStyle": { "backgroundColor": "blue" },
3406 "onClick": true,
3407 "src": "a.png",
3408 }));
3409 let (dirty, ev) = cached.merge_delta(
3410 props(serde_json::json!({ "style": { "width": 100 } })),
3411 &[],
3412 &[],
3413 );
3414
3415 let style = cached.style.as_ref().unwrap();
3416 assert_eq!(style.width.static_val(), Some(Length::Px(100.0)));
3417 assert_eq!(
3418 style.background_color.static_ref().map(String::as_str),
3419 Some("red")
3420 );
3421 assert!(style.outline.is_some(), "untouched style fields preserved");
3422 assert!(cached.hover_style.is_some(), "untouched props preserved");
3423 assert!(cached.on_click);
3424 assert_eq!(cached.src.as_deref(), Some("a.png"));
3425
3426 assert!(dirty.style.intersects(style_groups::LAYOUT));
3427 assert!(
3428 !dirty
3429 .style
3430 .intersects(style_groups::BACKGROUND | style_groups::OUTLINE),
3431 "untouched groups must stay clean"
3432 );
3433 assert!(!dirty.hover_style && !dirty.pointer && !dirty.image);
3434 assert!(dirty.style.intersects(style_groups::TRANSITION));
3436 assert!(ev.value.is_none() && ev.draw.is_none());
3437 }
3438
3439 #[test]
3442 fn merge_delta_unsets() {
3443 let mut cached = props(serde_json::json!({
3444 "style": { "backgroundColor": "red", "width": 50 },
3445 "hoverStyle": { "backgroundColor": "blue" },
3446 "onClick": true,
3447 }));
3448 let (dirty, _) = cached.merge_delta(
3449 Props::default(),
3450 &["hoverStyle".into(), "onClick".into()],
3451 &["backgroundColor".into()],
3452 );
3453
3454 let style = cached.style.as_ref().unwrap();
3455 assert_eq!(style.background_color, None);
3456 assert_eq!(
3457 style.width.static_val(),
3458 Some(Length::Px(50.0)),
3459 "other style fields kept"
3460 );
3461 assert!(cached.hover_style.is_none());
3462 assert!(!cached.on_click);
3463 assert!(dirty.style.intersects(style_groups::BACKGROUND));
3464 assert!(!dirty.style.intersects(style_groups::LAYOUT));
3465 assert!(dirty.hover_style && dirty.pointer);
3466 assert!(dirty.any_style_variant());
3467 }
3468
3469 #[test]
3475 fn merge_delta_bool_false_is_noop_off_rides_unset() {
3476 let mut cached = props(serde_json::json!({ "flipX": true, "flipY": true }));
3477
3478 let (dirty, _) = cached.merge_delta(props(serde_json::json!({ "flipX": false })), &[], &[]);
3480 assert!(cached.flip_x, "explicit false in a delta must not clear");
3481 assert!(!dirty.image);
3482
3483 let (dirty, _) = cached.merge_delta(Props::default(), &["flipX".into()], &[]);
3485 assert!(!cached.flip_x);
3486 assert!(cached.flip_y, "sibling flag untouched");
3487 assert!(dirty.image);
3488 }
3489
3490 #[test]
3492 fn merge_delta_unsets_style_wholesale() {
3493 let mut cached = props(serde_json::json!({
3494 "style": { "backgroundColor": "red", "width": 50 },
3495 }));
3496 let (dirty, _) = cached.merge_delta(Props::default(), &["style".into()], &[]);
3497 assert!(cached.style.is_none());
3498 assert_eq!(dirty.style, StyleDirty::ALL);
3499 }
3500
3501 #[test]
3503 fn merge_delta_events_not_cached() {
3504 let mut cached = Props::default();
3505 let (dirty, ev) = cached.merge_delta(
3506 props(serde_json::json!({
3507 "value": "hi", "selectionStart": 1, "selectionEnd": 3,
3508 "scrollTop": 40.0, "scrollLeft": 2.0,
3509 })),
3510 &[],
3511 &[],
3512 );
3513 assert_eq!(ev.value.as_deref(), Some("hi"));
3514 assert_eq!((ev.selection_start, ev.selection_end), (Some(1), Some(3)));
3515 assert_eq!((ev.scroll_top, ev.scroll_left), (Some(40.0), Some(2.0)));
3516 assert!(cached.value.is_none() && cached.scroll_top.is_none());
3517 assert!(cached.selection_start.is_none());
3518 assert!(!dirty.style.any() && !dirty.image && !dirty.anchor);
3520 }
3521
3522 #[test]
3525 fn merge_delta_replaces_variants_atomically() {
3526 let mut cached = props(serde_json::json!({
3527 "hoverStyle": { "backgroundColor": "blue", "width": 10 },
3528 }));
3529 let (dirty, _) = cached.merge_delta(
3530 props(serde_json::json!({ "hoverStyle": { "outline": { "color": "white" } } })),
3531 &[],
3532 &[],
3533 );
3534 let hover = cached.hover_style.as_ref().unwrap();
3535 assert!(hover.outline.is_some());
3536 assert_eq!(hover.background_color, None, "atomic replace, not a merge");
3537 assert_eq!(hover.width, None);
3538 assert!(dirty.hover_style);
3539 }
3540
3541 #[test]
3544 fn merge_delta_ignores_unknown_names() {
3545 let mut cached = props(serde_json::json!({ "style": { "width": 10 } }));
3546 let (dirty, _) = cached.merge_delta(
3547 Props::default(),
3548 &["nope".into(), "value".into()],
3549 &["alsoNope".into()],
3550 );
3551 assert_eq!(
3552 cached.style.as_ref().unwrap().width.static_val(),
3553 Some(Length::Px(10.0))
3554 );
3555 assert!(!dirty.style.any());
3556 }
3557
3558 #[test]
3560 fn merge_delta_converges() {
3561 let base = serde_json::json!({
3562 "style": { "backgroundColor": "red", "width": 10 }, "onClick": true,
3563 });
3564 let mut two_steps = props(base.clone());
3565 two_steps.merge_delta(
3566 props(serde_json::json!({ "style": { "width": 20 } })),
3567 &[],
3568 &[],
3569 );
3570 two_steps.merge_delta(
3571 props(serde_json::json!({ "style": { "height": 5 } })),
3572 &[],
3573 &["backgroundColor".into()],
3574 );
3575
3576 let mut one_step = props(base);
3577 one_step.merge_delta(
3578 props(serde_json::json!({ "style": { "width": 20, "height": 5 } })),
3579 &[],
3580 &["backgroundColor".into()],
3581 );
3582
3583 let a = two_steps.style.as_ref().unwrap();
3584 let b = one_step.style.as_ref().unwrap();
3585 assert_eq!(a.width, b.width);
3586 assert_eq!(a.height, b.height);
3587 assert_eq!(a.background_color, b.background_color);
3588 assert!(two_steps.on_click && one_step.on_click);
3589 }
3590
3591 #[test]
3593 fn split_events_strips_event_fields() {
3594 let full = props(serde_json::json!({
3595 "style": { "width": 10 }, "onClick": true, "value": "v",
3596 "selectionStart": 0, "selectionEnd": 1, "scrollTop": 5.0,
3597 }));
3598 let (state, ev) = full.split_events();
3599 assert!(state.style.is_some() && state.on_click);
3600 assert!(state.value.is_none() && state.selection_start.is_none());
3601 assert!(state.scroll_top.is_none());
3602 assert_eq!(ev.value.as_deref(), Some("v"));
3603 assert_eq!(ev.scroll_top, Some(5.0));
3604 }
3605
3606 #[test]
3610 fn deserializes_update_delta_form() {
3611 let minimal: Op = serde_json::from_str(r#"{"op":"update","id":3,"props":{}}"#).unwrap();
3612 match minimal {
3613 Op::Update {
3614 unset, style_unset, ..
3615 } => {
3616 assert!(unset.is_empty() && style_unset.is_empty());
3617 }
3618 other => panic!("expected update, got {other:?}"),
3619 }
3620 let full: Op = serde_json::from_str(
3621 r#"{"op":"update","id":3,"props":{"style":{"width":1}},
3622 "unset":["onClick"],"styleUnset":["backgroundColor"]}"#,
3623 )
3624 .unwrap();
3625 match full {
3626 Op::Update {
3627 unset, style_unset, ..
3628 } => {
3629 assert_eq!(unset, vec!["onClick"]);
3630 assert_eq!(style_unset, vec!["backgroundColor"]);
3631 }
3632 other => panic!("expected update, got {other:?}"),
3633 }
3634 }
3635
3636 #[test]
3640 fn deserializes_draw_op() {
3641 let op: Op = serde_json::from_str(
3642 r##"{"op":"draw","id":7,"cmds":[
3643 {"cmd":"clear"},
3644 {"cmd":"clearRect","x":1.0,"y":2.0,"w":3.0,"h":4.0},
3645 {"cmd":"fillStyle","color":"#f00"}
3646 ]}"##,
3647 )
3648 .unwrap();
3649 match op {
3650 Op::Draw { id, cmds } => {
3651 assert_eq!(id, 7);
3652 assert_eq!(cmds.len(), 3);
3653 assert_eq!(cmds[0], DrawCmd::Clear);
3654 assert_eq!(
3655 cmds[1],
3656 DrawCmd::ClearRect {
3657 x: 1.0,
3658 y: 2.0,
3659 w: 3.0,
3660 h: 4.0
3661 }
3662 );
3663 assert_eq!(
3664 cmds[2],
3665 DrawCmd::FillStyle {
3666 color: "#f00".into()
3667 }
3668 );
3669 }
3670 other => panic!("expected draw, got {other:?}"),
3671 }
3672 }
3673
3674 #[test]
3677 fn serializes_resize_ui_event() {
3678 let v = serde_json::to_value(Outbound::UiEvent {
3679 event: UiEvent {
3680 id: 5,
3681 kind: "resize".into(),
3682 width: Some(300.0),
3683 height: Some(150.0),
3684 ..Default::default()
3685 },
3686 })
3687 .unwrap();
3688 assert_eq!(v["t"], "uiEvent");
3689 let ev = &v["event"];
3690 assert_eq!(ev["id"], 5);
3691 assert_eq!(ev["kind"], "resize");
3692 assert_eq!(ev["width"], 300.0);
3693 assert_eq!(ev["height"], 150.0);
3694 assert!(ev.get("x").is_none() && ev.get("scrollTop").is_none());
3695 }
3696
3697 #[test]
3700 fn merge_delta_on_resize_flag() {
3701 let mut cached = Props::default();
3702 let (dirty, _) =
3703 cached.merge_delta(props(serde_json::json!({ "onResize": true })), &[], &[]);
3704 assert!(cached.on_resize);
3705 assert!(!dirty.pointer && !dirty.scroll_listener);
3706 cached.merge_delta(Props::default(), &["onResize".into()], &[]);
3707 assert!(!cached.on_resize);
3708 }
3709
3710 #[test]
3713 fn merge_delta_wheel_flag() {
3714 let mut cached = Props::default();
3715 let (dirty, _) =
3716 cached.merge_delta(props(serde_json::json!({ "onWheel": true })), &[], &[]);
3717 assert!(cached.on_wheel);
3718 assert!(dirty.wheel);
3719 assert!(!dirty.pointer && !dirty.scroll_listener);
3720
3721 let (dirty, _) = cached.merge_delta(Props::default(), &["onWheel".into()], &[]);
3722 assert!(!cached.on_wheel);
3723 assert!(dirty.wheel);
3724 }
3725
3726 #[test]
3729 fn deserializes_cursor_name() {
3730 let s: Style = serde_json::from_str(r#"{ "cursor": "pointer" }"#).expect("cursor decodes");
3731 assert_eq!(s.cursor.as_deref(), Some("pointer"));
3732
3733 let s: Style =
3734 serde_json::from_str(r#"{ "cursor": "hand" }"#).expect("custom name decodes");
3735 assert_eq!(s.cursor.as_deref(), Some("hand"));
3736 }
3737
3738 #[test]
3741 fn merge_delta_cursor_group() {
3742 let mut cached = Props::default();
3743 let (dirty, _) = cached.merge_delta(
3744 props(serde_json::json!({ "style": { "cursor": "pointer" } })),
3745 &[],
3746 &[],
3747 );
3748 assert_eq!(
3749 cached.style.as_ref().unwrap().cursor.as_deref(),
3750 Some("pointer")
3751 );
3752 assert!(dirty.style.intersects(style_groups::CURSOR));
3753 assert!(!dirty.style.intersects(style_groups::LAYOUT));
3754
3755 let (dirty, _) = cached.merge_delta(Props::default(), &[], &["cursor".into()]);
3756 assert_eq!(cached.style.as_ref().unwrap().cursor, None);
3757 assert!(dirty.style.intersects(style_groups::CURSOR));
3758 }
3759}