Rename Taffy struct to TaffyTree (#580)
1use std::fs::{self, OpenOptions};
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use convert_case::{Case, Casing};
7use fantoccini::{Client, ClientBuilder};
8use log::*;
9use proc_macro2::{Span, TokenStream};
10use quote::{format_ident, quote};
11use serde_json::Value;
12use syn::Ident;
13use walkdir::WalkDir;
14
15macro_rules! dim_quoted_renamed {
16 ($obj:ident, $in_name:ident, $out_name:ident, $value_mapper:ident, $default:expr) => {
17 let $out_name = match $obj.get(stringify!($in_name)) {
18 Some(Value::Object(ref value)) => {
19 let dim = $value_mapper(value);
20 quote!($out_name: #dim,)
21 }
22 _ => {
23 let dim = $default;
24 quote!($out_name: #dim,)
25 }
26 };
27 };
28}
29
30macro_rules! dim_quoted {
31 ($obj:ident, $dim_name:ident, $value_mapper: ident, $default:expr) => {
32 dim_quoted_renamed!($obj, $dim_name, $dim_name, $value_mapper, $default)
33 };
34}
35
36macro_rules! edges_quoted {
37 ($style:ident, $val:ident, $value_mapper:ident, $default_value: expr) => {
38 let $val = match $style[stringify!($val)] {
39 Value::Object(ref value) => {
40 dim_quoted!(value, left, $value_mapper, $default_value);
41 dim_quoted!(value, right, $value_mapper, $default_value);
42 dim_quoted!(value, top, $value_mapper, $default_value);
43 dim_quoted!(value, bottom, $value_mapper, $default_value);
44
45 let edges = quote!(taffy::geometry::Rect {
46 #left #right #top #bottom
47 });
48
49 quote!($val: #edges,)
50 },
51 _ => quote!(),
52 };
53 };
54}
55
56#[tokio::main]
57async fn main() {
58 env_logger::init();
59 let root_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
60 let repo_root = root_dir.parent().and_then(Path::parent).unwrap();
61
62 let fixtures_root = repo_root.join("test_fixtures");
63
64 info!("reading test fixtures from disk");
65 let mut fixtures: Vec<_> = WalkDir::new(fixtures_root.clone())
66 .into_iter()
67 .filter_map(|a| a.ok())
68 .filter(|f| !f.file_name().to_string_lossy().starts_with('x')) // ignore tests beginning with x
69 .filter(|f| f.path().is_file() && f.path().extension().map(|p| p == "html").unwrap_or(false))
70 .map(|f| {
71 let fixture_path = f.path().to_path_buf();
72 let name = fixture_path.file_stem().unwrap().to_str().unwrap().to_string();
73 (name, fixture_path)
74 })
75 .collect();
76 fixtures.sort_unstable_by_key(|f| f.0.clone());
77
78 info!("starting webdriver instance");
79 let webdriver_url = "http://localhost:4444";
80 let mut webdriver_handle = Command::new("chromedriver")
81 .arg("--port=4444")
82 .spawn()
83 .expect("ChromeDriver not found: Make sure you have it installed and added to your PATH.");
84
85 // this is silly, but it works
86 std::thread::sleep(std::time::Duration::from_secs(1));
87
88 let mut caps = serde_json::map::Map::new();
89 let chrome_opts = serde_json::json!({ "args": ["--headless", "--disable-gpu"] });
90 caps.insert("goog:chromeOptions".to_string(), chrome_opts.clone());
91
92 info!("spawning webdriver client and collecting test descriptions");
93 let client = ClientBuilder::native().capabilities(caps.clone()).connect(webdriver_url).await.unwrap();
94
95 asserts_non_zero_width_scrollbars(client.clone()).await;
96
97 let mut test_descs = vec![];
98 for (name, fixture_path) in fixtures.iter() {
99 test_descs.push(test_root_element(client.clone(), name.clone(), fixture_path).await);
100 }
101
102 info!("killing webdriver instance...");
103 webdriver_handle.kill().unwrap();
104
105 info!("generating test sources and concatenating...");
106
107 let test_descs: Vec<_> = test_descs
108 .iter()
109 .map(|(name, fixture_path, description)| {
110 debug!("generating test contents for {}", &name);
111 (name.clone(), fixture_path, generate_test(name, description))
112 })
113 .collect();
114
115 info!("writing generated test file to disk...");
116 let tests_base_path = repo_root.join("tests").join("generated");
117 fs::remove_dir_all(&tests_base_path).unwrap();
118 fs::create_dir(&tests_base_path).unwrap();
119
120 // Open base mod.rs file for appending
121 let mut base_mod_file = OpenOptions::new().create(true).append(true).open(tests_base_path.join("mod.rs")).unwrap();
122
123 for (name, fixture_path, test_body) in test_descs {
124 // Create test directory if it doesn't exist
125 let test_path_stripped = fixture_path.parent().unwrap().strip_prefix(&fixtures_root).unwrap();
126 let test_path = tests_base_path.join(test_path_stripped);
127 if !test_path.exists() {
128 fs::create_dir(&test_path).unwrap();
129
130 let ident = Ident::new(test_path_stripped.to_str().unwrap(), Span::call_site());
131 let token = quote!(mod #ident;);
132 writeln!(&mut base_mod_file, "{}", token).unwrap();
133 }
134 // Open mod file in current folder
135 let mod_path = test_path.join("mod.rs");
136 let mut mod_file = OpenOptions::new().create(true).append(true).open(mod_path).unwrap();
137
138 let name_ident = Ident::new(&name, Span::call_site());
139 let token = if name.starts_with("grid") {
140 quote!(#[cfg(feature = "grid")] mod #name_ident;)
141 } else {
142 quote!(mod #name_ident;)
143 };
144 writeln!(&mut mod_file, "{}", token).unwrap();
145 let mut test_filename = test_path.join(&name);
146 test_filename.set_extension("rs");
147 debug!("writing {} to disk...", &name);
148 fs::write(test_filename, test_body.to_string()).unwrap();
149 }
150
151 info!("formatting the source directory");
152 Command::new("cargo").arg("fmt").current_dir(repo_root).status().unwrap();
153}
154
155async fn asserts_non_zero_width_scrollbars(client: Client) {
156 // Load minimal test page defined in the string
157 const TEST_PAGE: &str = r#"data:text/html;charset=utf-8,<html><body><div style="overflow:scroll" /></body></html>"#;
158 client.goto(TEST_PAGE).await.unwrap();
159
160 // Determine the width of the scrollbar
161 let scrollbar_width = client
162 .execute("return document.body.firstChild.clientWidth - document.body.firstChild.offsetWidth;", vec![])
163 .await
164 .unwrap();
165 let Value::Number(scrollbar_width) = scrollbar_width else { panic!("Error retrieving scrollbar_width") };
166 let scrollbar_width = scrollbar_width.as_f64().unwrap();
167
168 if scrollbar_width == 0.0 {
169 panic!(concat!(
170 "\n\n Error: Scrollbar width of zero detected. This test generation script must be run with scrollbars set to take up space.\n",
171 " On macOS this can be done by setting Show Scrollbars to 'always' in the Appearance section of the System Settings app.\n\n"
172 ))
173 }
174}
175
176async fn test_root_element(client: Client, name: String, fixture_path: impl AsRef<Path>) -> (String, PathBuf, Value) {
177 let fixture_path = fixture_path.as_ref();
178
179 let url = format!("file://{}", fixture_path.display());
180
181 client.goto(&url).await.unwrap();
182 let description = client
183 .execute("return JSON.stringify(describeElement(document.getElementById('test-root')))", vec![])
184 .await
185 .unwrap();
186 let description_string = description.as_str().unwrap();
187 let description = serde_json::from_str(description_string).unwrap();
188 (name.to_string(), fixture_path.to_path_buf(), description)
189}
190
191fn generate_test(name: impl AsRef<str>, description: &Value) -> TokenStream {
192 let name = name.as_ref();
193 let name = Ident::new(name, Span::call_site());
194 let use_rounding = description["useRounding"].as_bool().unwrap();
195 let assertions = generate_assertions("node", description, use_rounding);
196 let node_description = generate_node("node", description);
197
198 let set_rounding_mode = if use_rounding { quote!() } else { quote!(taffy.disable_rounding();) };
199
200 // Compute available space
201 let viewport = &description["viewport"];
202 let available_space = if viewport["width"]["unit"] == "max-content" && viewport["height"]["unit"] == "max-content" {
203 quote!(taffy::geometry::Size::MAX_CONTENT)
204 } else {
205 dim_quoted!(viewport, width, generate_available_space, quote!(taffy::style::AvailableSpace::MAX_CONTENT));
206 dim_quoted!(viewport, height, generate_available_space, quote!(taffy::style::AvailableSpace::MAX_CONTENT));
207 quote!(
208 taffy::geometry::Size {
209 #width #height
210 }
211 )
212 };
213
214 quote!(
215 #[test]
216 fn #name() {
217 #[allow(unused_imports)]
218 use taffy::{tree::Layout, prelude::*, TaffyTree};
219 let mut taffy : TaffyTree<crate::TextMeasure> = TaffyTree::new();
220 #set_rounding_mode
221 #node_description
222 taffy.compute_layout_with_measure(node, #available_space, crate::test_measure_function).unwrap();
223
224 println!("\nComputed tree:");
225 taffy.print_tree(node);
226 println!();
227
228 #assertions
229 }
230 )
231}
232
233fn generate_assertions(ident: &str, node: &Value, use_rounding: bool) -> TokenStream {
234 let layout = if use_rounding { &node["smartRoundedLayout"] } else { &node["unroundedLayout"] };
235
236 let read_f32 = |s: &str| layout[s].as_f64().unwrap() as f32;
237 let read_naive_f32 = |s: &str| node["naivelyRoundedLayout"][s].as_f64().unwrap() as f32;
238 let width = read_f32("width");
239 let height = read_f32("height");
240 let x = read_f32("x");
241 let y = read_f32("y");
242
243 let scroll_width = (read_f32("scrollWidth") - read_naive_f32("clientWidth")).max(0.0);
244 let scroll_height = (read_f32("scrollHeight") - read_naive_f32("clientHeight")).max(0.0);
245
246 let children = {
247 let mut c = Vec::new();
248 if let Value::Array(ref value) = node["children"] {
249 for (idx, child) in value.iter().enumerate() {
250 c.push(generate_assertions(&format!("{ident}{idx}"), child, use_rounding));
251 }
252 };
253 c.into_iter().fold(quote!(), |a, b| quote!(#a #b))
254 };
255
256 let ident = Ident::new(ident, Span::call_site());
257
258 if use_rounding {
259 quote!(
260 #[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
261 let layout @ Layout { size, location, .. } = taffy.layout(#ident).unwrap();
262 assert_eq!(size.width, #width, "width of node {:?}. Expected {}. Actual {}", #ident, #width, size.width);
263 assert_eq!(size.height, #height, "height of node {:?}. Expected {}. Actual {}", #ident, #height, size.height);
264 assert_eq!(location.x, #x, "x of node {:?}. Expected {}. Actual {}", #ident, #x, location.x);
265 assert_eq!(location.y, #y, "y of node {:?}. Expected {}. Actual {}", #ident, #y, location.y);
266 #[cfg(feature = "content_size")]
267 assert_eq!(layout.scroll_width(), #scroll_width, "scroll_width of node {:?}. Expected {}. Actual {}", #ident, #scroll_width, layout.scroll_width());
268 #[cfg(feature = "content_size")]
269 assert_eq!(layout.scroll_height(), #scroll_height, "scroll_height of node {:?}. Expected {}. Actual {}", #ident, #scroll_height, layout.scroll_height());
270
271 #children
272 )
273 } else {
274 quote!(
275 #[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
276 let layout @ Layout { size, location, .. } = taffy.layout(#ident).unwrap();
277 assert!((size.width - #width).abs() < 0.1, "width of node {:?}. Expected {}. Actual {}", #ident, #width, size.width);
278 assert!((size.height - #height).abs() < 0.1, "height of node {:?}. Expected {}. Actual {}", #ident, #height, size.height);
279 assert!((location.x - #x).abs() < 0.1, "x of node {:?}. Expected {}. Actual {}", #ident, #x, location.x);
280 assert!((location.y - #y).abs() < 0.1, "y of node {:?}. Expected {}. Actual {}", #ident, #y, location.y);
281 #[cfg(feature = "content_size")]
282 assert!((layout.scroll_width() - #scroll_width).abs() < 0.1, "scroll_width of node {:?}. Expected {}. Actual {}", #ident, #scroll_width, layout.scroll_width());
283 #[cfg(feature = "content_size")]
284 assert!((layout.scroll_height() - #scroll_height).abs() < 0.1, "scroll_height of node {:?}. Expected {}. Actual {}", #ident, #scroll_height, layout.scroll_height());
285
286 #children
287 )
288 }
289}
290
291fn generate_node(ident: &str, node: &Value) -> TokenStream {
292 let style = &node["style"];
293
294 fn snake_case_ident(ident_name: &str) -> Ident {
295 let name_snake_case = ident_name.to_case(Case::Snake);
296 format_ident!("{}", name_snake_case)
297 }
298
299 fn quote_object_value(
300 prop_name: &str,
301 style: &Value,
302 quoter: impl Fn(&serde_json::Map<String, Value>) -> TokenStream,
303 ) -> Option<TokenStream> {
304 match style[prop_name.to_case(Case::Camel)] {
305 Value::Object(ref value) => Some(quoter(value)),
306 _ => None,
307 }
308 }
309
310 fn quote_prop(prop_name: &str, value: TokenStream) -> TokenStream {
311 let ident = snake_case_ident(prop_name);
312 quote!(#ident: #value,)
313 }
314
315 fn quote_object_prop(
316 prop_name: &str,
317 style: &Value,
318 quoter: impl Fn(&serde_json::Map<String, Value>) -> TokenStream,
319 ) -> TokenStream {
320 match quote_object_value(prop_name, style, quoter) {
321 Some(prop_value) => quote_prop(prop_name, prop_value),
322 None => quote!(),
323 }
324 }
325
326 fn quote_array_prop(prop_name: &str, style: &Value, quoter: impl Fn(&[Value]) -> TokenStream) -> TokenStream {
327 let prop_name_snake_case = prop_name.to_case(Case::Snake);
328 let prop_name_camel_case = prop_name.to_case(Case::Camel);
329 let prop_name_ident = format_ident!("{}", prop_name_snake_case);
330 match style[prop_name_camel_case] {
331 Value::Array(ref value) => {
332 let prop_value = quoter(value);
333 quote!(#prop_name_ident: #prop_value,)
334 }
335 _ => quote!(),
336 }
337 }
338
339 fn get_string_value<'a, 'b, 'c: 'b>(prop_name: &'a str, style: &'c Value) -> Option<&'b str> {
340 match style[prop_name.to_case(Case::Camel)] {
341 Value::String(ref value) => Some(value),
342 _ => None,
343 }
344 }
345
346 fn get_number_value<'a, 'b, 'c: 'b>(prop_name: &'a str, style: &'c Value) -> Option<f32> {
347 match style[prop_name.to_case(Case::Camel)] {
348 Value::Number(ref value) => Some(value.as_f64().unwrap() as f32),
349 _ => None,
350 }
351 }
352
353 fn quote_number_prop(prop_name: &str, style: &Value, quoter: impl Fn(f32) -> TokenStream) -> TokenStream {
354 let prop_name_snake_case = prop_name.to_case(Case::Snake);
355 let prop_name_camel_case = prop_name.to_case(Case::Camel);
356 let prop_name_ident = format_ident!("{}", prop_name_snake_case);
357 match style[prop_name_camel_case] {
358 Value::Number(ref value) => {
359 let prop_value = quoter(value.as_f64().unwrap() as f32);
360 quote!(#prop_name_ident: #prop_value,)
361 }
362 _ => quote!(),
363 }
364 }
365
366 let display = match style["display"] {
367 Value::String(ref value) => match value.as_ref() {
368 "none" => quote!(display: taffy::style::Display::None,),
369 "block" => quote!(display: taffy::style::Display::Block,),
370 "grid" => quote!(display: taffy::style::Display::Grid,),
371 _ => quote!(display: taffy::style::Display::Flex,),
372 },
373 _ => quote!(),
374 };
375
376 let position = match style["position"] {
377 Value::String(ref value) => match value.as_ref() {
378 "absolute" => quote!(position: taffy::style::Position::Absolute,),
379 _ => quote!(),
380 },
381 _ => quote!(),
382 };
383
384 let direction = match style["direction"] {
385 Value::String(ref value) => match value.as_ref() {
386 "rtl" => quote!(direction: taffy::style::Direction::RTL,),
387 "ltr" => quote!(direction: taffy::style::Direction::LTR,),
388 _ => quote!(),
389 },
390 _ => quote!(),
391 };
392
393 let flex_direction = match style["flexDirection"] {
394 Value::String(ref value) => match value.as_ref() {
395 "row-reverse" => quote!(flex_direction: taffy::style::FlexDirection::RowReverse,),
396 "column" => quote!(flex_direction: taffy::style::FlexDirection::Column,),
397 "column-reverse" => quote!(flex_direction: taffy::style::FlexDirection::ColumnReverse,),
398 _ => quote!(),
399 },
400 _ => quote!(),
401 };
402
403 let flex_wrap = match style["flexWrap"] {
404 Value::String(ref value) => match value.as_ref() {
405 "wrap" => quote!(flex_wrap: taffy::style::FlexWrap::Wrap,),
406 "wrap-reverse" => quote!(flex_wrap: taffy::style::FlexWrap::WrapReverse,),
407 _ => quote!(),
408 },
409 _ => quote!(),
410 };
411
412 fn quote_overflow(overflow: &Value) -> Option<TokenStream> {
413 match overflow {
414 Value::String(ref value) => match value.as_ref() {
415 "hidden" => Some(quote!(taffy::style::Overflow::Hidden)),
416 "scroll" => Some(quote!(taffy::style::Overflow::Scroll)),
417 "auto" => Some(quote!(taffy::style::Overflow::Auto)),
418 _ => None,
419 },
420 _ => None,
421 }
422 }
423 let overflow_x = quote_overflow(&style["overflowX"]);
424 let overflow_y = quote_overflow(&style["overflowY"]);
425 let (overflow, scrollbar_width) = if overflow_x.is_some() || overflow_y.is_some() {
426 let overflow_x = overflow_x.unwrap_or(quote!(taffy::style::Overflow::Visible));
427 let overflow_y = overflow_y.unwrap_or(quote!(taffy::style::Overflow::Visible));
428 let overflow = quote!(overflow: taffy::geometry::Point { x: #overflow_x, y: #overflow_y },);
429 let scrollbar_width = quote_number_prop("scrollbar_width", style, |value: f32| quote!(#value));
430 (overflow, scrollbar_width)
431 } else {
432 (quote!(), quote!())
433 };
434
435 let align_items = match style["alignItems"] {
436 Value::String(ref value) => match value.as_ref() {
437 "start" => quote!(align_items: Some(taffy::style::AlignItems::Start),),
438 "end" => quote!(align_items: Some(taffy::style::AlignItems::End),),
439 "flex-start" => quote!(align_items: Some(taffy::style::AlignItems::FlexStart),),
440 "flex-end" => quote!(align_items: Some(taffy::style::AlignItems::FlexEnd),),
441 "center" => quote!(align_items: Some(taffy::style::AlignItems::Center),),
442 "baseline" => quote!(align_items: Some(taffy::style::AlignItems::Baseline),),
443 "stretch" => quote!(align_items: Some(taffy::style::AlignItems::Stretch),),
444 _ => quote!(),
445 },
446 _ => quote!(),
447 };
448
449 let align_self = match style["alignSelf"] {
450 Value::String(ref value) => match value.as_ref() {
451 "start" => quote!(align_self: Some(taffy::style::AlignSelf::Start),),
452 "end" => quote!(align_self: Some(taffy::style::AlignSelf::End),),
453 "flex-start" => quote!(align_self: Some(taffy::style::AlignSelf::FlexStart),),
454 "flex-end" => quote!(align_self: Some(taffy::style::AlignSelf::FlexEnd),),
455 "center" => quote!(align_self: Some(taffy::style::AlignSelf::Center),),
456 "baseline" => quote!(align_self: Some(taffy::style::AlignSelf::Baseline),),
457 "stretch" => quote!(align_self: Some(taffy::style::AlignSelf::Stretch),),
458 _ => quote!(),
459 },
460 _ => quote!(),
461 };
462
463 let justify_items = match style["justifyItems"] {
464 Value::String(ref value) => match value.as_ref() {
465 "start" => quote!(justify_items: Some(taffy::style::JustifyItems::Start),),
466 "end" => quote!(justify_items: Some(taffy::style::JustifyItems::End),),
467 "flex-start" => quote!(justify_items: Some(taffy::style::JustifyItems::FlexStart),),
468 "flex-end" => quote!(justify_items: Some(taffy::style::JustifyItems::FlexEnd),),
469 "center" => quote!(justify_items: Some(taffy::style::JustifyItems::Center),),
470 "baseline" => quote!(justify_items: Some(taffy::style::JustifyItems::Baseline),),
471 "stretch" => quote!(justify_items: Some(taffy::style::JustifyItems::Stretch),),
472 _ => quote!(),
473 },
474 _ => quote!(),
475 };
476
477 let justify_self = match style["justifySelf"] {
478 Value::String(ref value) => match value.as_ref() {
479 "start" => quote!(justify_self: Some(taffy::style::JustifySelf::Start),),
480 "end" => quote!(justify_self: Some(taffy::style::JustifySelf::End),),
481 "flex-start" => quote!(justify_self: Some(taffy::style::JustifySelf::FlexStart),),
482 "flex-end" => quote!(justify_self: Some(taffy::style::JustifySelf::FlexEnd),),
483 "center" => quote!(justify_self: Some(taffy::style::JustifySelf::Center),),
484 "baseline" => quote!(justify_self: Some(taffy::style::JustifySelf::Baseline),),
485 "stretch" => quote!(justify_self: Some(taffy::style::JustifySelf::Stretch),),
486 _ => quote!(),
487 },
488 _ => quote!(),
489 };
490
491 let align_content = match style["alignContent"] {
492 Value::String(ref value) => match value.as_ref() {
493 "start" => quote!(align_content: Some(taffy::style::AlignContent::Start),),
494 "end" => quote!(align_content: Some(taffy::style::AlignContent::End),),
495 "flex-start" => quote!(align_content: Some(taffy::style::AlignContent::FlexStart),),
496 "flex-end" => quote!(align_content: Some(taffy::style::AlignContent::FlexEnd),),
497 "center" => quote!(align_content: Some(taffy::style::AlignContent::Center),),
498 "stretch" => quote!(align_content: Some(taffy::style::AlignContent::Stretch),),
499 "space-between" => quote!(align_content: Some(taffy::style::AlignContent::SpaceBetween),),
500 "space-around" => quote!(align_content: Some(taffy::style::AlignContent::SpaceAround),),
501 "space-evenly" => quote!(align_content: Some(taffy::style::AlignContent::SpaceEvenly),),
502 _ => quote!(),
503 },
504 _ => quote!(),
505 };
506
507 let justify_content = match style["justifyContent"] {
508 Value::String(ref value) => match value.as_ref() {
509 "start" => quote!(justify_content: Some(taffy::style::JustifyContent::Start),),
510 "end" => quote!(justify_content: Some(taffy::style::JustifyContent::End),),
511 "flex-start" => quote!(justify_content: Some(taffy::style::JustifyContent::FlexStart),),
512 "flex-end" => quote!(justify_content: Some(taffy::style::JustifyContent::FlexEnd),),
513 "center" => quote!(justify_content: Some(taffy::style::JustifyContent::Center),),
514 "stretch" => quote!(justify_content: Some(taffy::style::AlignContent::Stretch),),
515 "space-between" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceBetween),),
516 "space-around" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceAround),),
517 "space-evenly" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceEvenly),),
518 _ => quote!(),
519 },
520 _ => quote!(),
521 };
522
523 let flex_grow = quote_number_prop("flex_grow", style, |value: f32| quote!(#value));
524 let flex_shrink = quote_number_prop("flex_shrink", style, |value: f32| quote!(#value));
525
526 let flex_basis = quote_object_prop("flex_basis", style, generate_dimension);
527 let size = quote_object_prop("size", style, generate_size);
528 let min_size = quote_object_prop("min_size", style, generate_size);
529 let max_size = quote_object_prop("max_size", style, generate_size);
530 let aspect_ratio = quote_number_prop("aspect_ratio", style, |value: f32| quote!(Some(#value)));
531
532 let gap = quote_object_prop("gap", style, generate_gap);
533
534 let grid_template_rows = quote_array_prop("grid_template_rows", style, generate_track_definition_list);
535 let grid_template_columns = quote_array_prop("grid_template_columns", style, generate_track_definition_list);
536 let grid_auto_rows = quote_array_prop("grid_auto_rows", style, generate_track_definition_list);
537 let grid_auto_columns = quote_array_prop("grid_auto_columns", style, generate_track_definition_list);
538 let grid_auto_flow = quote_object_prop("grid_auto_flow", style, generate_grid_auto_flow);
539
540 let default_grid_placement = quote!(taffy::style::GridPlacement::Auto);
541
542 let grid_row_start = quote_object_value("grid_row_start", style, generate_grid_position);
543 let grid_row_end = quote_object_value("grid_row_end", style, generate_grid_position);
544 let grid_row = if grid_row_start.is_some() || grid_row_end.is_some() {
545 quote_prop(
546 "grid_row",
547 generate_line(
548 grid_row_start.unwrap_or(default_grid_placement.clone()),
549 grid_row_end.unwrap_or(default_grid_placement.clone()),
550 ),
551 )
552 } else {
553 quote!()
554 };
555
556 let grid_column_start = quote_object_value("grid_column_start", style, generate_grid_position);
557 let grid_column_end = quote_object_value("grid_column_end", style, generate_grid_position);
558 let grid_column = if grid_column_start.is_some() || grid_column_end.is_some() {
559 quote_prop(
560 "grid_column",
561 generate_line(
562 grid_column_start.unwrap_or(default_grid_placement.clone()),
563 grid_column_end.unwrap_or(default_grid_placement),
564 ),
565 )
566 } else {
567 quote!()
568 };
569
570 let text_content = get_string_value("text_content", node);
571 let writing_mode = get_string_value("writingMode", style);
572 let raw_aspect_ratio = get_number_value("aspect_ratio", style);
573 let node_context: Option<_> = text_content.map(|text| generate_node_context(text, writing_mode, raw_aspect_ratio));
574
575 edges_quoted!(style, margin, generate_length_percentage_auto, quote!(zero()));
576 edges_quoted!(style, padding, generate_length_percentage, quote!(zero()));
577 edges_quoted!(style, border, generate_length_percentage, quote!(zero()));
578 edges_quoted!(style, inset, generate_length_percentage_auto, quote!(auto()));
579
580 // Quote children
581 let child_descriptions: Vec<Value> = match node["children"] {
582 Value::Array(ref value) => value.clone(),
583 _ => vec![],
584 };
585 let has_children = !child_descriptions.is_empty();
586 let (children_body, children) = if has_children {
587 let body = child_descriptions
588 .iter()
589 .enumerate()
590 .map(|(i, child)| generate_node(&format!("{ident}{i}"), child))
591 .collect();
592 let idents = child_descriptions
593 .iter()
594 .enumerate()
595 .map(|(i, _)| Ident::new(&format!("{ident}{i}"), Span::call_site()))
596 .collect::<Vec<_>>();
597 (body, quote!(&[#(#idents),*]))
598 } else {
599 (quote!(), quote!())
600 };
601
602 let ident = Ident::new(ident, Span::call_site());
603
604 let style = quote!(taffy::style::Style {
605 #display
606 #direction
607 #position
608 #flex_direction
609 #flex_wrap
610 #overflow
611 #scrollbar_width
612 #align_items
613 #align_self
614 #justify_items
615 #justify_self
616 #align_content
617 #justify_content
618 #flex_grow
619 #flex_shrink
620 #flex_basis
621 #gap
622 #grid_template_rows
623 #grid_template_columns
624 #grid_auto_rows
625 #grid_auto_columns
626 #grid_auto_flow
627 #grid_row
628 #grid_column
629 #size
630 #min_size
631 #max_size
632 #aspect_ratio
633 #margin
634 #padding
635 #inset
636 #border
637 ..Default::default()
638 });
639
640 if has_children {
641 quote!(
642 #children_body
643 let #ident = taffy.new_with_children(#style,#children).unwrap();
644 )
645 } else if node_context.is_some() {
646 quote!(let #ident = taffy.new_leaf_with_context(#style,#node_context,).unwrap();)
647 } else {
648 quote!(let #ident = taffy.new_leaf(#style).unwrap();)
649 }
650}
651
652fn generate_size(size: &serde_json::Map<String, Value>) -> TokenStream {
653 dim_quoted!(size, width, generate_dimension, quote!(auto()));
654 dim_quoted!(size, height, generate_dimension, quote!(auto()));
655 quote!(
656 taffy::geometry::Size {
657 #width #height
658 }
659 )
660}
661
662fn generate_gap(size: &serde_json::Map<String, Value>) -> TokenStream {
663 dim_quoted_renamed!(size, column, width, generate_length_percentage, quote!(zero()));
664 dim_quoted_renamed!(size, row, height, generate_length_percentage, quote!(zero()));
665 quote!(
666 taffy::geometry::Size {
667 #width #height
668 }
669 )
670}
671
672fn generate_length_percentage(dimen: &serde_json::Map<String, Value>) -> TokenStream {
673 let unit = dimen.get("unit").unwrap();
674 let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
675
676 match unit {
677 Value::String(ref unit) => match unit.as_ref() {
678 "px" => {
679 let value = value();
680 quote!(taffy::style::LengthPercentage::Length(#value))
681 }
682 "percent" => {
683 let value = value();
684 quote!(taffy::style::LengthPercentage::Percent(#value))
685 }
686 _ => unreachable!(),
687 },
688 _ => unreachable!(),
689 }
690}
691
692fn generate_length_percentage_auto(dimen: &serde_json::Map<String, Value>) -> TokenStream {
693 let unit = dimen.get("unit").unwrap();
694 let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
695
696 match unit {
697 Value::String(ref unit) => match unit.as_ref() {
698 "auto" => quote!(taffy::style::LengthPercentageAuto::Auto),
699 "px" => {
700 let value = value();
701 quote!(taffy::style::LengthPercentageAuto::Length(#value))
702 }
703 "percent" => {
704 let value = value();
705 quote!(taffy::style::LengthPercentageAuto::Percent(#value))
706 }
707 _ => unreachable!(),
708 },
709 _ => unreachable!(),
710 }
711}
712
713fn generate_dimension(dimen: &serde_json::Map<String, Value>) -> TokenStream {
714 let unit = dimen.get("unit").unwrap();
715 let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
716
717 match unit {
718 Value::String(ref unit) => match unit.as_ref() {
719 "auto" => quote!(taffy::style::Dimension::Auto),
720 "px" => {
721 let value = value();
722 quote!(taffy::style::Dimension::Length(#value))
723 }
724 "percent" => {
725 let value = value();
726 quote!(taffy::style::Dimension::Percent(#value))
727 }
728 _ => unreachable!(),
729 },
730 _ => unreachable!(),
731 }
732}
733
734fn generate_available_space(dimen: &serde_json::Map<String, Value>) -> TokenStream {
735 let unit = dimen.get("unit").unwrap();
736 let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
737
738 match unit {
739 Value::String(ref unit) => match unit.as_ref() {
740 "max-content" => quote!(taffy::style::AvailableSpace::MaxContent),
741 "min-content" => quote!(taffy::style::AvailableSpace::MaxContent),
742 "px" => {
743 let value = value();
744 quote!(taffy::style::AvailableSpace::Definite(#value))
745 }
746 _ => unreachable!(),
747 },
748 _ => unreachable!(),
749 }
750}
751
752fn generate_grid_auto_flow(auto_flow: &serde_json::Map<String, Value>) -> TokenStream {
753 let direction = auto_flow.get("direction").unwrap().as_str().unwrap();
754 let algorithm = auto_flow.get("algorithm").unwrap().as_str().unwrap();
755
756 match (direction, algorithm) {
757 ("row", "sparse") => quote!(taffy::style::GridAutoFlow::Row),
758 ("column", "sparse") => quote!(taffy::style::GridAutoFlow::Column),
759 ("row", "dense") => quote!(taffy::style::GridAutoFlow::RowDense),
760 ("column", "dense") => quote!(taffy::style::GridAutoFlow::ColumnDense),
761 _ => unreachable!(),
762 }
763}
764
765fn generate_line(start: TokenStream, end: TokenStream) -> TokenStream {
766 quote!(taffy::geometry::Line { start:#start, end:#end })
767}
768
769fn generate_grid_position(grid_position: &serde_json::Map<String, Value>) -> TokenStream {
770 let kind = grid_position.get("kind").unwrap();
771 let value = || grid_position.get("value").unwrap().as_f64().unwrap() as f32;
772
773 match kind {
774 Value::String(ref kind) => match kind.as_ref() {
775 "auto" => quote!(taffy::style::GridPlacement::Auto),
776 "span" => {
777 let value = value() as u16;
778 quote!(taffy::style::GridPlacement::Span(#value))
779 }
780 "line" => {
781 let value = value() as i16;
782 quote!(line(#value))
783 }
784 _ => unreachable!(),
785 },
786 _ => unreachable!(),
787 }
788}
789
790fn generate_track_definition_list(raw_list: &[Value]) -> TokenStream {
791 let list = raw_list.iter().map(|obj| match obj {
792 Value::Object(inner) => generate_track_definition(inner),
793 _ => unreachable!(),
794 });
795
796 quote!(vec![#(#list),*])
797}
798
799fn generate_track_definition(track_definition: &serde_json::Map<String, Value>) -> TokenStream {
800 let kind = track_definition.get("kind").unwrap().as_str().unwrap();
801 let name = || track_definition.get("name").unwrap().as_str().unwrap();
802 let arguments = || track_definition.get("arguments").unwrap();
803
804 match kind {
805 "scalar" => generate_scalar_definition(track_definition),
806 "function" => match (name(), arguments()) {
807 ("fit-content", Value::Array(arguments)) => {
808 if arguments.len() != 1 {
809 panic!("fit-content function with the wrong number of arguments");
810 }
811 let argument = match arguments[0] {
812 Value::Object(ref arg) => generate_scalar_definition(arg),
813 _ => unreachable!(),
814 };
815 quote!(fit_content(#argument))
816 }
817 ("minmax", Value::Array(arguments)) => {
818 if arguments.len() != 2 {
819 panic!("minmax function with the wrong number of arguments");
820 }
821 let min = match arguments[0] {
822 Value::Object(ref arg) => generate_scalar_definition(arg),
823 _ => unreachable!(),
824 };
825 let max = match arguments[1] {
826 Value::Object(ref arg) => generate_scalar_definition(arg),
827 _ => unreachable!(),
828 };
829 quote!(minmax(#min, #max))
830 }
831 ("repeat", Value::Array(arguments)) => {
832 if arguments.len() < 2 {
833 panic!("repeat function with the wrong number of arguments");
834 }
835 let repetition = match arguments[0] {
836 Value::Object(ref arg) => {
837 let unit = arg.get("unit").unwrap().as_str().unwrap();
838 let value = || arg.get("value").unwrap().as_u64().unwrap() as u16;
839
840 match unit {
841 "auto-fill" => quote!(GridTrackRepetition::AutoFill),
842 "auto-fit" => quote!(GridTrackRepetition::AutoFit),
843 "integer" => {
844 let repetition_count = value();
845 quote!(GridTrackRepetition::Count(#repetition_count))
846 }
847 _ => unreachable!(),
848 }
849 }
850 _ => unreachable!(),
851 };
852 let track_list = generate_track_definition_list(&arguments[1..]);
853 quote!(repeat(#repetition, #track_list))
854 }
855 // TODO: Add support for fit-content
856 _ => unreachable!(),
857 },
858 _ => unreachable!(),
859 }
860}
861
862fn generate_scalar_definition(track_definition: &serde_json::Map<String, Value>) -> TokenStream {
863 let unit = || track_definition.get("unit").unwrap().as_str().unwrap();
864 let value = || track_definition.get("value").unwrap().as_f64().unwrap() as f32;
865
866 match unit() {
867 "auto" => quote!(auto()),
868 "min-content" => quote!(min_content()),
869 "max-content" => quote!(max_content()),
870 "px" | "percent" | "fraction" => {
871 let value = value();
872 match unit() {
873 "px" => quote!(length(#value)),
874 "percent" => quote!(percent(#value)),
875 "fraction" => quote!(fr(#value)),
876 _ => unreachable!(),
877 }
878 }
879 _ => unreachable!(),
880 }
881}
882
883fn generate_node_context(text_content: &str, writing_mode: Option<&str>, aspect_ratio: Option<f32>) -> TokenStream {
884 let trimmed_text_content = text_content.trim();
885
886 let writing_mode_token = match writing_mode {
887 Some("vertical-rl" | "vertical-lr") => quote!(crate::WritingMode::Vertical),
888 _ => quote!(crate::WritingMode::Horizontal),
889 };
890
891 let aspect_ratio_token = match aspect_ratio {
892 Some(ratio) => quote!(Some(#ratio)),
893 None => quote!(None),
894 };
895
896 quote!(
897 crate::TextMeasure {
898 text_content: #trimmed_text_content,
899 writing_mode: #writing_mode_token,
900 _aspect_ratio: #aspect_ratio_token,
901 }
902 )
903}