tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 583HTML 275Markdown 8TOML 41586 Definiciones

⎇ 9958624168dfe671c82cbe3afb8ba6a7d35f7454 scripts/gentest/src/main.rs Rust · 955 líneas · 38.1 KB Historia Quién tocó qué crudo

Fix: Make CSS Grid algorithm correctly apply max width/height and available space when it is the root node (#491) 995862416 · Nico Burns ·

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