1use serde::{Deserialize, Serialize};
17use serde_json::{Map, Value};
18use taffy::prelude::*;
19use taffy::style::{AlignContent, AlignItems, BoxSizing, Direction, Display, FlexDirection, FlexWrap, LengthPercentage, LengthPercentageAuto, Overflow, Position, TextAlign};
20
21pub use crate::conditions::{is_condition_key, resolve_conditions, ConditionContext};
22use crate::tracks;
23
24#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
26pub struct ShadowLayer {
27 pub x: f32,
28 pub y: f32,
29 pub blur: f32,
30 pub spread: f32,
31 pub color: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
36pub struct GradientStop {
37 pub color: String,
38 pub offset: f32,
39}
40
41#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
44pub struct Gradient {
45 pub angle: f32,
46 pub stops: Vec<GradientStop>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
52#[serde(tag = "op", rename_all = "camelCase")]
53pub enum TransformOp {
54 Translate { x: f32, y: f32 },
55 Scale { factor: f32 },
56 Rotate { degrees: f32 },
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
63pub struct Transition {
64 pub duration_ms: f32,
65 pub easing: [f32; 4],
66}
67
68pub const DEFAULT_EASING: [f32; 4] = [0.25, 0.1, 0.25, 1.0];
70
71#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
74pub struct Visual {
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub background_color: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub background_gradient: Option<Gradient>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub color: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
84 pub border_width: Option<f32>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub border_widths: Option<[f32; 4]>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub border_color: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub border_style: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub border_radius: Option<f32>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub corner_radii: Option<[f32; 4]>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub opacity: Option<f32>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub box_shadow: Option<Vec<ShadowLayer>>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub font_size: Option<f32>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub font_weight: Option<u16>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub line_height: Option<f32>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub font_family: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
114 pub text_align: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub letter_spacing: Option<f32>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub text_decoration: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub text_transform: Option<String>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub font_style: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
130 pub padding_horizontal: Option<f32>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub padding_vertical: Option<f32>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub padding: Option<[f32; 4]>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub gap: Option<f32>,
138 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
141 pub native: bool,
142 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
143 pub overflow_hidden: bool,
144 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
145 pub overflow_scroll: bool,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub overflow_x: Option<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub overflow_y: Option<String>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub transform: Option<Vec<TransformOp>>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub transition: Option<Transition>,
156 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
158 pub visibility_hidden: bool,
159 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
163 pub display_none: bool,
164 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
166 pub pointer_events_none: bool,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub user_select: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub cursor: Option<String>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub series_colors: Option<Vec<String>>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub direction: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub backdrop_blur: Option<f32>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub animation: Option<VisualAnimation>,
188 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
191 pub sticky: bool,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct VisualAnimation {
198 pub name: String,
199 pub timing: crate::animation::AnimationTiming,
200 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
202 pub reduced: bool,
203}
204
205pub fn resolve_logical(style: &mut Map<String, Value>, direction: Direction) {
210 let (start, end) = match direction {
211 Direction::Rtl => ("Right", "Left"),
212 _ => ("Left", "Right"),
213 };
214 for (logical, prefix) in [
215 ("marginInlineStart", "margin"),
216 ("marginInlineEnd", "margin"),
217 ("paddingInlineStart", "padding"),
218 ("paddingInlineEnd", "padding"),
219 ("insetInlineStart", ""),
220 ("insetInlineEnd", ""),
221 ] {
222 if let Some(v) = style.shift_remove(logical) {
223 let side = if logical.ends_with("Start") { start } else { end };
224 let physical = if prefix.is_empty() { side.to_lowercase() } else { format!("{prefix}{side}") };
225 style.insert(physical, v);
226 }
227 }
228 if let Some(v) = style.shift_remove("insetBlockStart") {
229 style.insert("top".into(), v);
230 }
231 if let Some(v) = style.shift_remove("insetBlockEnd") {
232 style.insert("bottom".into(), v);
233 }
234 let rtl = matches!(direction, Direction::Rtl);
235 match style.get("textAlign").and_then(Value::as_str) {
236 Some("start") => {
237 style.insert("textAlign".into(), Value::String(if rtl { "right" } else { "left" }.into()));
238 }
239 Some("end") => {
240 style.insert("textAlign".into(), Value::String(if rtl { "left" } else { "right" }.into()));
241 }
242 _ => {}
243 }
244}
245
246fn num(v: &Value) -> Option<f32> {
247 v.as_f64().map(|n| n as f32)
248}
249
250fn percent_of(s: &str) -> Option<f32> {
251 s.strip_suffix('%').and_then(|n| n.trim().parse::<f32>().ok()).map(|n| n / 100.0)
252}
253
254fn px_of(s: &str) -> Option<f32> {
255 s.strip_suffix("px").and_then(|n| n.trim().parse::<f32>().ok())
256}
257
258fn dimension(v: &Value) -> Option<Dimension> {
259 if let Some(n) = num(v) {
260 return Some(length(n));
261 }
262 match v.as_str()? {
263 "auto" => Some(Dimension::auto()),
264 s => percent_of(s).map(percent).or_else(|| px_of(s).map(length)),
265 }
266}
267
268fn lpa(v: &Value) -> Option<LengthPercentageAuto> {
269 if let Some(n) = num(v) {
270 return Some(length(n));
271 }
272 match v.as_str()? {
273 "auto" => Some(LengthPercentageAuto::auto()),
274 s => percent_of(s).map(percent).or_else(|| px_of(s).map(length)),
275 }
276}
277
278fn lp(v: &Value) -> Option<LengthPercentage> {
279 if let Some(n) = num(v) {
280 return Some(length(n));
281 }
282 let s = v.as_str()?;
283 percent_of(s).map(percent).or_else(|| px_of(s).map(length))
284}
285
286pub fn px(v: &Value) -> Option<f32> {
288 if let Some(n) = num(v) {
289 return Some(n);
290 }
291 v.as_str().and_then(px_of)
292}
293
294pub fn percent_value(v: &Value) -> Option<f32> {
296 v.as_str().and_then(percent_of)
297}
298
299fn rect_lp(r: &Map<String, Value>, all: &str, horizontal: &str, vertical: &str, sides: [&str; 4]) -> Rect<LengthPercentage> {
300 let mut out: Rect<LengthPercentage> = Rect::zero();
301 if let Some(v) = r.get(all).and_then(lp) {
302 out = Rect { left: v, right: v, top: v, bottom: v };
303 }
304 if let Some(v) = r.get(horizontal).and_then(lp) {
305 out.left = v;
306 out.right = v;
307 }
308 if let Some(v) = r.get(vertical).and_then(lp) {
309 out.top = v;
310 out.bottom = v;
311 }
312 let [t, rr, b, l] = sides;
313 if let Some(v) = r.get(t).and_then(lp) {
314 out.top = v;
315 }
316 if let Some(v) = r.get(rr).and_then(lp) {
317 out.right = v;
318 }
319 if let Some(v) = r.get(b).and_then(lp) {
320 out.bottom = v;
321 }
322 if let Some(v) = r.get(l).and_then(lp) {
323 out.left = v;
324 }
325 out
326}
327
328fn rect_lpa(r: &Map<String, Value>, all: &str, horizontal: &str, vertical: &str, sides: [&str; 4], default: LengthPercentageAuto) -> Rect<LengthPercentageAuto> {
329 let mut out = Rect { left: default, right: default, top: default, bottom: default };
330 if let Some(v) = r.get(all).and_then(lpa) {
331 out = Rect { left: v, right: v, top: v, bottom: v };
332 }
333 if let Some(v) = r.get(horizontal).and_then(lpa) {
334 out.left = v;
335 out.right = v;
336 }
337 if let Some(v) = r.get(vertical).and_then(lpa) {
338 out.top = v;
339 out.bottom = v;
340 }
341 let [t, rr, b, l] = sides;
342 if let Some(v) = r.get(t).and_then(lpa) {
343 out.top = v;
344 }
345 if let Some(v) = r.get(rr).and_then(lpa) {
346 out.right = v;
347 }
348 if let Some(v) = r.get(b).and_then(lpa) {
349 out.bottom = v;
350 }
351 if let Some(v) = r.get(l).and_then(lpa) {
352 out.left = v;
353 }
354 out
355}
356
357pub fn sides_px(r: &Map<String, Value>, all: &str, horizontal: &str, vertical: &str, sides: [&str; 4]) -> Option<[f32; 4]> {
361 let keys = [all, horizontal, vertical, sides[0], sides[1], sides[2], sides[3]];
362 if !keys.iter().any(|k| r.contains_key(*k)) {
363 return None;
364 }
365 let mut out = [0.0f32; 4];
366 if let Some(v) = r.get(all).and_then(px) {
367 out = [v; 4];
368 }
369 if let Some(v) = r.get(horizontal).and_then(px) {
370 out[1] = v;
371 out[3] = v;
372 }
373 if let Some(v) = r.get(vertical).and_then(px) {
374 out[0] = v;
375 out[2] = v;
376 }
377 for (i, k) in sides.iter().enumerate() {
378 if let Some(v) = r.get(*k).and_then(px) {
379 out[i] = v;
380 }
381 }
382 Some(out)
383}
384
385pub fn border_sides(r: &Map<String, Value>) -> Option<[f32; 4]> {
388 let sides = ["borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"];
389 if !r.contains_key("borderWidth") && !sides.iter().any(|k| r.contains_key(*k)) {
390 return None;
391 }
392 let all = r.get("borderWidth").and_then(num).unwrap_or(0.0);
393 let mut out = [all; 4];
394 for (i, k) in sides.iter().enumerate() {
395 if let Some(v) = r.get(*k).and_then(num) {
396 out[i] = v;
397 }
398 }
399 Some(out)
400}
401
402fn align_items(s: &str) -> Option<AlignItems> {
403 Some(match s {
404 "flex-start" | "start" => AlignItems::FLEX_START,
405 "flex-end" | "end" => AlignItems::FLEX_END,
406 "center" => AlignItems::CENTER,
407 "stretch" => AlignItems::STRETCH,
408 "baseline" => AlignItems::BASELINE,
409 _ => return None,
410 })
411}
412
413fn align_content(s: &str) -> Option<AlignContent> {
414 Some(match s {
415 "flex-start" | "start" => AlignContent::FLEX_START,
416 "flex-end" | "end" => AlignContent::FLEX_END,
417 "center" => AlignContent::CENTER,
418 "stretch" => AlignContent::STRETCH,
419 "space-between" => AlignContent::SPACE_BETWEEN,
420 "space-around" => AlignContent::SPACE_AROUND,
421 "space-evenly" => AlignContent::SPACE_EVENLY,
422 _ => return None,
423 })
424}
425
426fn overflow_of(s: &str) -> Result<Overflow, String> {
427 Ok(match s {
428 "visible" => Overflow::Visible,
429 "hidden" | "clip" => Overflow::Clip,
430 "scroll" | "auto" => Overflow::Scroll,
431 other => return Err(format!("unsupported overflow {other:?}")),
432 })
433}
434
435pub fn axis_overflow<'a>(r: &'a Map<String, Value>, axis: &str) -> Option<&'a str> {
437 r.get(axis).or_else(|| r.get("overflow")).and_then(Value::as_str)
438}
439
440pub fn is_scroll_container(r: &Map<String, Value>) -> (bool, bool) {
442 let scrolls = |v: Option<&str>| matches!(v, Some("scroll") | Some("auto"));
443 (scrolls(axis_overflow(r, "overflowX")), scrolls(axis_overflow(r, "overflowY")))
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub enum BoxKind {
449 Container,
451 Leaf,
455}
456
457pub fn to_taffy(r: &Map<String, Value>, direction: Direction, kind: BoxKind) -> Result<Style, String> {
460 let mut s = Style { box_sizing: BoxSizing::BorderBox, direction, ..Style::default() };
461 let str_of = |k: &str| r.get(k).and_then(Value::as_str);
462
463 s.display = match str_of("display") {
464 None | Some("flex") => Display::Flex,
465 Some("grid") => Display::Grid,
466 Some("block") => Display::Block,
467 Some("none") => Display::None,
468 Some(other) => return Err(format!("unsupported display {other:?}")),
469 };
470 s.flex_direction = match str_of("flexDirection") {
471 None | Some("row") => FlexDirection::Row,
472 Some("column") => FlexDirection::Column,
473 Some("row-reverse") => FlexDirection::RowReverse,
474 Some("column-reverse") => FlexDirection::ColumnReverse,
475 Some(other) => return Err(format!("unsupported flexDirection {other:?}")),
476 };
477 s.flex_wrap = match str_of("flexWrap") {
478 None | Some("nowrap") => FlexWrap::NoWrap,
479 Some("wrap") => FlexWrap::Wrap,
480 Some("wrap-reverse") => FlexWrap::WrapReverse,
481 Some(other) => return Err(format!("unsupported flexWrap {other:?}")),
482 };
483 s.position = match str_of("position") {
484 None | Some("relative") => Position::Relative,
485 Some("absolute") => Position::Absolute,
486 Some("sticky") => Position::Relative,
489 Some(other) => return Err(format!("unsupported position {other:?}")),
490 };
491 let ox = axis_overflow(r, "overflowX").map(overflow_of).transpose()?;
492 let oy = axis_overflow(r, "overflowY").map(overflow_of).transpose()?;
493 s.overflow = taffy::geometry::Point { x: ox.unwrap_or(Overflow::Visible), y: oy.unwrap_or(Overflow::Visible) };
494 if let Some(v) = str_of("justifyContent") {
495 s.justify_content = Some(align_content(v).ok_or(format!("justifyContent {v:?}"))?);
496 }
497 if let Some(v) = str_of("alignContent") {
498 s.align_content = Some(align_content(v).ok_or(format!("alignContent {v:?}"))?);
499 }
500 if let Some(v) = str_of("alignItems") {
501 s.align_items = Some(align_items(v).ok_or(format!("alignItems {v:?}"))?);
502 }
503 if let Some(v) = str_of("alignSelf") {
504 s.align_self = Some(align_items(v).ok_or(format!("alignSelf {v:?}"))?);
505 }
506 if let Some(v) = str_of("justifySelf") {
507 s.justify_self = Some(align_items(v).ok_or(format!("justifySelf {v:?}"))?);
508 }
509 if let Some(v) = str_of("textAlign") {
510 s.text_align = match v {
511 "left" => TextAlign::LegacyLeft,
512 "right" => TextAlign::LegacyRight,
513 "center" => TextAlign::LegacyCenter,
514 _ => TextAlign::Auto,
515 };
516 }
517 if let Some(v) = r.get("flexGrow").and_then(num) {
518 s.flex_grow = v;
519 }
520 if let Some(v) = r.get("flexShrink").and_then(num) {
521 s.flex_shrink = v;
522 }
523 if let Some(v) = r.get("flexBasis").and_then(dimension) {
524 s.flex_basis = v;
525 }
526 let gap_all = r.get("gap").and_then(lp);
527 s.gap = Size {
528 width: r.get("columnGap").and_then(lp).or(gap_all).unwrap_or(LengthPercentage::ZERO),
529 height: r.get("rowGap").and_then(lp).or(gap_all).unwrap_or(LengthPercentage::ZERO),
530 };
531 s.size = Size {
532 width: r.get("width").and_then(dimension).unwrap_or(Dimension::auto()),
533 height: r.get("height").and_then(dimension).unwrap_or(Dimension::auto()),
534 };
535 s.min_size = Size {
536 width: r.get("minWidth").and_then(dimension).unwrap_or(Dimension::auto()),
537 height: r.get("minHeight").and_then(dimension).unwrap_or(Dimension::auto()),
538 };
539 s.max_size = Size {
540 width: r.get("maxWidth").and_then(dimension).unwrap_or(Dimension::auto()),
541 height: r.get("maxHeight").and_then(dimension).unwrap_or(Dimension::auto()),
542 };
543 if let Some(v) = r.get("aspectRatio") {
544 s.aspect_ratio = num(v).or_else(|| {
545 let (a, b) = v.as_str()?.split_once('/')?;
546 Some(a.trim().parse::<f32>().ok()? / b.trim().parse::<f32>().ok()?)
547 });
548 }
549 if str_of("position") != Some("sticky") {
550 s.inset = rect_lpa(r, "inset", "insetHorizontal", "insetVertical", ["top", "right", "bottom", "left"], LengthPercentageAuto::auto());
551 }
552 s.margin = rect_lpa(r, "margin", "marginHorizontal", "marginVertical", ["marginTop", "marginRight", "marginBottom", "marginLeft"], LengthPercentageAuto::ZERO);
553 if kind == BoxKind::Container {
554 s.padding = rect_lp(r, "padding", "paddingHorizontal", "paddingVertical", ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
555 if let Some([t, rr, b, l]) = border_sides(r) {
556 s.border = Rect { left: length(l), right: length(rr), top: length(t), bottom: length(b) };
557 }
558 }
559 if let Some(v) = str_of("gridTemplateColumns") {
560 s.grid_template_columns = tracks::parse_tracks(v)?;
561 }
562 if let Some(v) = str_of("gridTemplateRows") {
563 s.grid_template_rows = tracks::parse_tracks(v)?;
564 }
565 if let Some(rows) = r.get("gridTemplateAreas").and_then(Value::as_array) {
566 let rows: Vec<String> = rows.iter().filter_map(|v| v.as_str().map(str::to_string)).collect();
567 s.grid_template_areas = tracks::parse_areas(&rows)?;
568 }
569 if let Some(v) = str_of("gridArea") {
570 let l = tracks::parse_area_placement(v);
571 s.grid_row = l.clone();
572 s.grid_column = l;
573 }
574 if let Some(v) = str_of("gridColumn") {
575 s.grid_column = tracks::parse_placement(v)?;
576 }
577 if let Some(v) = str_of("gridRow") {
578 s.grid_row = tracks::parse_placement(v)?;
579 }
580 Ok(s)
581}
582
583fn shadow_layers(v: &Value) -> Option<Vec<ShadowLayer>> {
584 let layers = v.as_array()?;
585 Some(
586 layers
587 .iter()
588 .filter_map(|l| {
589 let o = l.as_object()?;
590 Some(ShadowLayer {
591 x: o.get("x").and_then(num).unwrap_or(0.0),
592 y: o.get("y").and_then(num).unwrap_or(0.0),
593 blur: o.get("blur").and_then(num).unwrap_or(0.0),
594 spread: o.get("spread").and_then(num).unwrap_or(0.0),
595 color: o.get("color").and_then(Value::as_str).unwrap_or("#00000000").to_string(),
596 })
597 })
598 .collect(),
599 )
600}
601
602fn gradient(v: &Value) -> Option<Gradient> {
603 let o = v.as_object()?;
604 let stops = o
605 .get("stops")?
606 .as_array()?
607 .iter()
608 .filter_map(|s| Some(GradientStop { color: s.get("color")?.as_str()?.to_string(), offset: s.get("offset").and_then(num).unwrap_or(0.0) }))
609 .collect::<Vec<_>>();
610 (stops.len() >= 2).then(|| Gradient { angle: o.get("angle").and_then(num).unwrap_or(180.0), stops })
611}
612
613pub fn parse_transform(s: &str) -> Option<Vec<TransformOp>> {
615 if !crate::style_check::is_transform(s) {
616 return None;
617 }
618 let mut out = Vec::new();
619 let mut rest = s;
620 while let Some(close) = rest.find(')') {
621 let (piece, after) = rest.split_at(close + 1);
622 if let Some(args) = piece.strip_prefix("translate(").and_then(|r| r.strip_suffix(')')) {
623 let (x, y) = args.split_once(", ")?;
624 out.push(TransformOp::Translate { x: x.strip_suffix("px")?.parse().ok()?, y: y.strip_suffix("px")?.parse().ok()? });
625 } else if let Some(n) = piece.strip_prefix("scale(").and_then(|r| r.strip_suffix(')')) {
626 out.push(TransformOp::Scale { factor: n.parse().ok()? });
627 } else if let Some(n) = piece.strip_prefix("rotate(").and_then(|r| r.strip_suffix(')')) {
628 out.push(TransformOp::Rotate { degrees: n.strip_suffix("deg")?.parse().ok()? });
629 }
630 rest = after.strip_prefix(' ').unwrap_or(after);
631 }
632 Some(out)
633}
634
635fn easing(v: &Value) -> Option<[f32; 4]> {
636 let a = v.as_array()?;
637 if a.len() != 4 {
638 return None;
639 }
640 Some([num(&a[0])?, num(&a[1])?, num(&a[2])?, num(&a[3])?])
641}
642
643fn avg(a: f32, b: f32) -> f32 {
645 (a + b) / 2.0
646}
647
648pub fn visual(r: &Map<String, Value>, kind: BoxKind) -> Visual {
652 let str_of = |k: &str| r.get(k).and_then(Value::as_str).map(str::to_string);
653 let leaf = kind == BoxKind::Leaf;
654 let pads = if leaf { sides_px(r, "padding", "paddingHorizontal", "paddingVertical", ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]) } else { None };
655 let borders = border_sides(r);
656 let uniform_border = borders.filter(|b| b.iter().all(|w| (w - b[0]).abs() < 0.001)).map(|b| b[0]);
657 let corners = ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"];
658 let radius = r.get("borderRadius").and_then(px);
659 let corner_radii = corners.iter().any(|k| r.get(*k).and_then(px).is_some()).then(|| {
660 let base = radius.unwrap_or(0.0);
661 let mut out = [base; 4];
662 for (i, k) in corners.iter().enumerate() {
663 if let Some(v) = r.get(*k).and_then(px) {
664 out[i] = v;
665 }
666 }
667 out
668 });
669 let ox = axis_overflow(r, "overflowX").map(str::to_string);
670 let oy = axis_overflow(r, "overflowY").map(str::to_string);
671 let any = |v: &Option<String>, set: &[&str]| v.as_deref().is_some_and(|x| set.contains(&x));
672 let transition = r.get("transition").and_then(num).map(|ms| Transition { duration_ms: ms, easing: r.get("transitionEasing").and_then(easing).unwrap_or(DEFAULT_EASING) });
673 Visual {
674 background_color: str_of("backgroundColor"),
675 background_gradient: r.get("backgroundGradient").and_then(gradient),
676 color: str_of("color"),
677 border_width: uniform_border.or_else(|| r.get("borderWidth").and_then(num)),
678 border_widths: borders.filter(|_| uniform_border.is_none()),
679 border_color: str_of("borderColor"),
680 border_style: str_of("borderStyle"),
681 border_radius: radius,
682 corner_radii,
683 opacity: r.get("opacity").and_then(num),
684 box_shadow: r.get("boxShadow").and_then(shadow_layers),
685 font_size: r.get("fontSize").and_then(px),
686 font_weight: r.get("fontWeight").and_then(|v| v.as_u64()).map(|w| w as u16),
687 line_height: r.get("lineHeight").and_then(px),
688 font_family: str_of("fontFamily"),
689 text_align: str_of("textAlign"),
690 letter_spacing: r.get("letterSpacing").and_then(px),
691 text_decoration: str_of("textDecoration"),
692 text_transform: str_of("textTransform"),
693 font_style: str_of("fontStyle"),
694 padding_horizontal: pads.map(|p| avg(p[1], p[3])),
695 padding_vertical: pads.map(|p| avg(p[0], p[2])),
696 padding: pads,
697 gap: if leaf { r.get("gap").and_then(px) } else { None },
698 native: r.get("native").and_then(Value::as_bool).unwrap_or(false),
699 overflow_hidden: any(&ox, &["hidden", "clip"]) || any(&oy, &["hidden", "clip"]),
700 overflow_scroll: any(&ox, &["scroll", "auto"]) || any(&oy, &["scroll", "auto"]),
701 overflow_x: ox,
702 overflow_y: oy,
703 transform: r.get("transform").and_then(Value::as_str).and_then(parse_transform),
704 transition,
705 visibility_hidden: r.get("visibility").and_then(Value::as_str) == Some("hidden"),
706 display_none: r.get("display").and_then(Value::as_str) == Some("none"),
707 pointer_events_none: r.get("pointerEvents").and_then(Value::as_str) == Some("none"),
708 user_select: str_of("userSelect"),
709 cursor: str_of("cursor"),
710 series_colors: None,
711 direction: None,
712 backdrop_blur: r.get("backdropBlur").and_then(px),
713 animation: None,
714 sticky: r.get("position").and_then(Value::as_str) == Some("sticky"),
715 }
716}
717
718pub fn direction_of(r: &Map<String, Value>) -> Option<Direction> {
721 match r.get("direction").and_then(Value::as_str) {
722 Some("rtl") => Some(Direction::Rtl),
723 Some("ltr") => Some(Direction::Ltr),
724 _ => None,
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731 use serde_json::json;
732
733 fn ctx(width: f32, states: &[&str]) -> ConditionContext {
734 ConditionContext { width, states: states.iter().map(|s| s.to_string()).collect(), breakpoints: crate::conditions::default_breakpoints(), ..Default::default() }
735 }
736
737 #[test]
738 fn media_and_states_flatten_in_source_order() {
739 let style = json!({"gap": 4, "@media (min-width: 600px)": {"gap": 8}, "@media (min-width: 900px)": {"gap": 12}, ":pressed": {"opacity": 0.6}});
740 let m = style.as_object().unwrap();
741 assert_eq!(resolve_conditions(m, &ctx(390.0, &[])).get("gap"), Some(&json!(4)));
742 let wide = resolve_conditions(m, &ctx(900.0, &["pressed"]));
743 assert_eq!(wide.get("gap"), Some(&json!(12)));
744 assert_eq!(wide.get("opacity"), Some(&json!(0.6)));
745 }
746
747 #[test]
748 fn logical_properties_follow_the_direction() {
749 let mut m = json!({"marginInlineStart": 8, "insetInlineEnd": 4, "paddingInlineEnd": 2, "insetBlockEnd": 3, "textAlign": "start"}).as_object().unwrap().clone();
750 resolve_logical(&mut m, Direction::Rtl);
751 assert_eq!(m.get("marginRight"), Some(&json!(8)));
752 assert_eq!(m.get("left"), Some(&json!(4)));
753 assert_eq!(m.get("paddingLeft"), Some(&json!(2)));
754 assert_eq!(m.get("bottom"), Some(&json!(3)));
755 assert_eq!(m.get("textAlign"), Some(&json!("right")));
756 assert!(m.get("marginInlineStart").is_none());
757 }
758
759 #[test]
760 fn leaf_padding_stays_out_of_taffy_with_every_side() {
761 let m = json!({"paddingHorizontal": 16, "paddingTop": 2, "paddingLeft": 10, "height": 36, "borderWidth": 1}).as_object().unwrap().clone();
762 let leaf = to_taffy(&m, Direction::Ltr, BoxKind::Leaf).unwrap();
763 assert_eq!(leaf.padding, Rect::zero());
764 assert_eq!(leaf.border, Rect::zero());
765 let container = to_taffy(&m, Direction::Ltr, BoxKind::Container).unwrap();
766 assert_eq!(container.padding.left, length(10.0));
767 assert_eq!(container.padding.right, length(16.0));
768 let v = visual(&m, BoxKind::Leaf);
769 assert_eq!(v.padding, Some([2.0, 16.0, 0.0, 10.0]));
770 assert_eq!(v.padding_horizontal, Some(13.0));
771 assert_eq!(v.padding_vertical, Some(1.0));
772 assert_eq!(v.border_width, Some(1.0));
773 }
774
775 #[test]
776 fn per_side_borders_reach_taffy_and_the_visual() {
777 let m = json!({"borderWidth": 1, "borderBottomWidth": 3, "borderStyle": "dashed", "borderRadius": 8, "borderTopLeftRadius": 2}).as_object().unwrap().clone();
778 let s = to_taffy(&m, Direction::Ltr, BoxKind::Container).unwrap();
779 assert_eq!(s.border.bottom, length(3.0));
780 assert_eq!(s.border.top, length(1.0));
781 let v = visual(&m, BoxKind::Container);
782 assert_eq!(v.border_widths, Some([1.0, 1.0, 3.0, 1.0]));
783 assert_eq!(v.border_width, Some(1.0));
784 assert_eq!(v.border_style.as_deref(), Some("dashed"));
785 assert_eq!(v.corner_radii, Some([2.0, 8.0, 8.0, 8.0]));
786 }
787
788 #[test]
789 fn overflow_per_axis_and_paint_only_keys() {
790 let m = json!({"overflowX": "scroll", "overflow": "hidden", "transform": "translate(4px, -2px) rotate(90deg)", "transition": 150, "transitionEasing": [0.2, 0, 0, 1], "visibility": "hidden", "pointerEvents": "none", "backgroundGradient": {"angle": 90, "stops": [{"color": "#ff0000", "offset": 0}, {"color": "#0000ff", "offset": 1}]}}).as_object().unwrap().clone();
791 let s = to_taffy(&m, Direction::Ltr, BoxKind::Container).unwrap();
792 assert_eq!(s.overflow.x, Overflow::Scroll);
793 assert_eq!(s.overflow.y, Overflow::Clip);
794 assert_eq!(is_scroll_container(&m), (true, false));
795 let v = visual(&m, BoxKind::Container);
796 assert_eq!(v.transform, Some(vec![TransformOp::Translate { x: 4.0, y: -2.0 }, TransformOp::Rotate { degrees: 90.0 }]));
797 assert_eq!(v.transition, Some(Transition { duration_ms: 150.0, easing: [0.2, 0.0, 0.0, 1.0] }));
798 assert!(v.visibility_hidden && v.pointer_events_none);
799 assert_eq!(v.background_gradient.as_ref().unwrap().stops.len(), 2);
800 }
801}