Implement "gap" property for flexbox algorithm (#248)
1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use convert_case::{Case, Casing};
6use fantoccini::{Client, ClientBuilder};
7
8use log::*;
9use proc_macro2::{Span, TokenStream};
10use quote::{format_ident, quote};
11use syn::Ident;
12
13#[tokio::main]
14async fn main() {
15 env_logger::init();
16 // this requires being run by cargo, which is iffy
17 let root_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
18 let repo_root = root_dir.parent().and_then(Path::parent).unwrap();
19
20 let fixtures_root = repo_root.join("test_fixtures");
21 let fixtures = fs::read_dir(fixtures_root).unwrap();
22
23 info!("reading test fixtures from disk");
24 let mut fixtures: Vec<_> = fixtures
25 .into_iter()
26 .filter_map(|a| a.ok())
27 .filter(|f| f.path().is_file() && f.path().extension().map(|p| p == "html").unwrap_or(false))
28 .map(|f| {
29 let fixture_path = f.path();
30 let name = fixture_path.file_stem().unwrap().to_str().unwrap().to_string();
31 (name, fixture_path)
32 })
33 .collect();
34 fixtures.sort_unstable_by_key(|f| f.0.clone());
35
36 info!("starting webdriver instance");
37 let webdriver_url = "http://localhost:4444";
38 let mut webdriver_handle = Command::new("chromedriver")
39 .arg("--port=4444")
40 .spawn()
41 .expect("ChromeDriver not found: Make sure you have it installed and added to your PATH.");
42
43 // this is silly, but it works
44 std::thread::sleep(std::time::Duration::from_secs(1));
45
46 let mut caps = serde_json::map::Map::new();
47 let chrome_opts = serde_json::json!({ "args": ["--headless", "--disable-gpu"] });
48 caps.insert("goog:chromeOptions".to_string(), chrome_opts.clone());
49
50 info!("spawning webdriver client and collecting test descriptions");
51 let client = ClientBuilder::native().capabilities(caps.clone()).connect(webdriver_url).await.unwrap();
52
53 let mut test_descs = vec![];
54 for (name, fixture_path) in fixtures {
55 test_descs.push(test_root_element(client.clone(), name, fixture_path).await);
56 }
57
58 info!("killing webdriver instance...");
59 webdriver_handle.kill().unwrap();
60
61 info!("generating test sources and concatenating...");
62
63 let bench_descs: Vec<_> = test_descs
64 .iter()
65 .map(|(name, description)| {
66 debug!("generating bench contents for {}", &name);
67 (name.clone(), generate_bench(description))
68 })
69 .collect();
70
71 let test_descs: Vec<_> = test_descs
72 .iter()
73 .map(|(name, description)| {
74 debug!("generating test contents for {}", &name);
75 (name.clone(), generate_test(name, description))
76 })
77 .collect();
78
79 let benchmarks: Vec<_> = test_descs
80 .iter()
81 .map(|(name, _)| {
82 let bench_mod = Ident::new(name, Span::call_site());
83 quote!(#bench_mod::compute())
84 })
85 .collect();
86
87 let test_mods = test_descs
88 .iter()
89 .map(|(name, _)| {
90 let name = Ident::new(name, Span::call_site());
91 quote!(mod #name;)
92 })
93 .fold(quote!(), |a, b| quote!(#a #b));
94
95 let bench_mods = quote!(
96 use criterion::{criterion_group, criterion_main, Criterion};
97
98 #test_mods
99
100 fn benchmark(c: &mut Criterion) {
101 c.bench_function("generated benchmarks", |b| {
102 b.iter(|| { #(#benchmarks;)* })
103 });
104 }
105
106 criterion_group!(benches, benchmark);
107 criterion_main!(benches);
108 );
109
110 info!("writing generated benchmarks file to disk...");
111 let benches_base_path = repo_root.join("benches").join("generated");
112 fs::remove_dir_all(&benches_base_path).unwrap();
113 fs::create_dir(&benches_base_path).unwrap();
114 for (name, bench_body) in bench_descs {
115 let mut bench_filename = benches_base_path.join(&name);
116 bench_filename.set_extension("rs");
117 debug!("writing {} to disk...", &name);
118 fs::write(bench_filename, bench_body.to_string()).unwrap();
119 }
120 fs::write(benches_base_path.join("mod.rs"), bench_mods.to_string()).unwrap();
121
122 info!("writing generated test file to disk...");
123 let tests_base_path = repo_root.join("tests").join("generated");
124 fs::remove_dir_all(&tests_base_path).unwrap();
125 fs::create_dir(&tests_base_path).unwrap();
126 for (name, test_body) in test_descs {
127 let mut test_filename = tests_base_path.join(&name);
128 test_filename.set_extension("rs");
129 debug!("writing {} to disk...", &name);
130 fs::write(test_filename, test_body.to_string()).unwrap();
131 }
132 fs::write(tests_base_path.join("mod.rs"), test_mods.to_string()).unwrap();
133
134 info!("formatting the source directory");
135 Command::new("cargo").arg("fmt").current_dir(repo_root).status().unwrap();
136}
137
138async fn test_root_element(client: Client, name: String, fixture_path: impl AsRef<Path>) -> (String, json::JsonValue) {
139 let fixture_path = fixture_path.as_ref();
140
141 let url = format!("file://{}", fixture_path.display());
142
143 client.goto(&url).await.unwrap();
144 let description = client
145 .execute("return JSON.stringify(describeElement(document.getElementById('test-root')))", vec![])
146 .await
147 .unwrap();
148 let description_string = description.as_str().unwrap();
149 let description = json::parse(description_string).unwrap();
150 (name, description)
151}
152
153fn generate_bench(description: &json::JsonValue) -> TokenStream {
154 let node_description = generate_node("node", description);
155
156 quote!(
157 pub fn compute() {
158 let mut taffy = taffy::Taffy::new();
159 #node_description
160 taffy.compute_layout(node, taffy::geometry::Size::MAX_CONTENT).unwrap();
161 }
162 )
163}
164
165fn generate_test(name: impl AsRef<str>, description: &json::JsonValue) -> TokenStream {
166 let name = name.as_ref();
167 let name = Ident::new(name, Span::call_site());
168 let node_description = generate_node("node", description);
169 let assertions = generate_assertions("node", description);
170
171 quote!(
172 #[test]
173 fn #name() {
174 let mut taffy = taffy::Taffy::new();
175 #node_description
176 taffy.compute_layout(node, taffy::geometry::Size::MAX_CONTENT).unwrap();
177 #assertions
178 }
179 )
180}
181
182fn generate_assertions(ident: &str, node: &json::JsonValue) -> TokenStream {
183 let layout = &node["layout"];
184
185 let read_f32 = |s: &str| layout[s].as_f32().unwrap();
186 let width = read_f32("width");
187 let height = read_f32("height");
188 let x = read_f32("x");
189 let y = read_f32("y");
190
191 let children = {
192 let mut c = Vec::new();
193 if let json::JsonValue::Array(ref value) = node["children"] {
194 for (idx, child) in value.iter().enumerate() {
195 c.push(generate_assertions(&format!("{}{}", ident, idx), child));
196 }
197 };
198 c.into_iter().fold(quote!(), |a, b| quote!(#a #b))
199 };
200
201 let ident = Ident::new(ident, Span::call_site());
202
203 quote!(
204 assert_eq!(taffy.layout(#ident).unwrap().size.width, #width);
205 assert_eq!(taffy.layout(#ident).unwrap().size.height, #height);
206 assert_eq!(taffy.layout(#ident).unwrap().location.x, #x);
207 assert_eq!(taffy.layout(#ident).unwrap().location.y, #y);
208
209 #children
210 )
211}
212
213fn generate_node(ident: &str, node: &json::JsonValue) -> TokenStream {
214 let style = &node["style"];
215
216 fn quote_object_prop(
217 prop_name: &str,
218 style: &json::JsonValue,
219 quoter: impl Fn(&json::object::Object) -> TokenStream,
220 ) -> TokenStream {
221 let prop_name_snake_case = prop_name.to_case(Case::Snake);
222 let prop_name_camel_case = prop_name.to_case(Case::Camel);
223 let prop_name_ident = format_ident!("{}", prop_name_snake_case);
224 match style[prop_name_camel_case] {
225 json::JsonValue::Object(ref value) => {
226 let prop_value = quoter(value);
227 quote!(#prop_name_ident: #prop_value,)
228 }
229 _ => quote!(),
230 }
231 }
232
233 fn quote_array_prop(
234 prop_name: &str,
235 style: &json::JsonValue,
236 quoter: impl Fn(&[json::JsonValue]) -> TokenStream,
237 ) -> TokenStream {
238 let prop_name_snake_case = prop_name.to_case(Case::Snake);
239 let prop_name_camel_case = prop_name.to_case(Case::Camel);
240 let prop_name_ident = format_ident!("{}", prop_name_snake_case);
241 match style[prop_name_camel_case] {
242 json::JsonValue::Array(ref value) => {
243 let prop_value = quoter(value);
244 quote!(#prop_name_ident: #prop_value,)
245 }
246 _ => quote!(),
247 }
248 }
249
250 // This is currently unused, but leaving for future use.
251 #[allow(dead_code)]
252 fn quote_string_prop(
253 prop_name: &str,
254 style: &json::JsonValue,
255 quoter: impl Fn(&str) -> TokenStream,
256 ) -> TokenStream {
257 let prop_name_snake_case = prop_name.to_case(Case::Snake);
258 let prop_name_camel_case = prop_name.to_case(Case::Camel);
259 let prop_name_ident = format_ident!("{}", prop_name_snake_case);
260 match style[prop_name_camel_case] {
261 json::JsonValue::Short(ref value) => {
262 let prop_value = quoter(value.as_ref());
263 quote!(#prop_name_ident: #prop_value,)
264 }
265 json::JsonValue::String(ref value) => {
266 let prop_value = quoter(value.as_ref());
267 quote!(#prop_name_ident: #prop_value,)
268 }
269 _ => quote!(),
270 }
271 }
272
273 fn quote_number_prop<T: From<json::number::Number>>(
274 prop_name: &str,
275 style: &json::JsonValue,
276 quoter: impl Fn(T) -> TokenStream,
277 ) -> TokenStream {
278 let prop_name_snake_case = prop_name.to_case(Case::Snake);
279 let prop_name_camel_case = prop_name.to_case(Case::Camel);
280 let prop_name_ident = format_ident!("{}", prop_name_snake_case);
281 match style[prop_name_camel_case] {
282 json::JsonValue::Number(ref value) => {
283 let prop_value = quoter((*value).into());
284 quote!(#prop_name_ident: #prop_value,)
285 }
286 _ => quote!(),
287 }
288 }
289
290 let display = match style["display"] {
291 json::JsonValue::Short(ref value) => match value.as_ref() {
292 "none" => quote!(display: taffy::style::Display::None,),
293 "grid" => quote!(display: taffy::style::Display::Grid,),
294 _ => quote!(display: taffy::style::Display::Flex,),
295 },
296 _ => quote!(),
297 };
298
299 let position_type = match style["positionType"] {
300 json::JsonValue::Short(ref value) => match value.as_ref() {
301 "absolute" => quote!(position_type: taffy::style::PositionType::Absolute,),
302 _ => quote!(),
303 },
304 _ => quote!(),
305 };
306
307 let direction = match style["direction"] {
308 json::JsonValue::Short(ref value) => match value.as_ref() {
309 "rtl" => quote!(direction: taffy::style::Direction::RTL,),
310 "ltr" => quote!(direction: taffy::style::Direction::LTR,),
311 _ => quote!(),
312 },
313 _ => quote!(),
314 };
315
316 let flex_direction = match style["flexDirection"] {
317 json::JsonValue::Short(ref value) => match value.as_ref() {
318 "row-reverse" => quote!(flex_direction: taffy::style::FlexDirection::RowReverse,),
319 "column" => quote!(flex_direction: taffy::style::FlexDirection::Column,),
320 "column-reverse" => quote!(flex_direction: taffy::style::FlexDirection::ColumnReverse,),
321 _ => quote!(),
322 },
323 _ => quote!(),
324 };
325
326 let flex_wrap = match style["flexWrap"] {
327 json::JsonValue::Short(ref value) => match value.as_ref() {
328 "wrap" => quote!(flex_wrap: taffy::style::FlexWrap::Wrap,),
329 "wrap-reverse" => quote!(flex_wrap: taffy::style::FlexWrap::WrapReverse,),
330 _ => quote!(),
331 },
332 _ => quote!(),
333 };
334
335 let overflow = match style["overflow"] {
336 json::JsonValue::Short(ref value) => match value.as_ref() {
337 "hidden" => quote!(overflow: taffy::style::Overflow::Hidden,),
338 "scroll" => quote!(overflow: taffy::style::Overflow::Scroll,),
339 _ => quote!(),
340 },
341 _ => quote!(),
342 };
343
344 let align_items = match style["alignItems"] {
345 json::JsonValue::Short(ref value) => match value.as_ref() {
346 "flex-start" => quote!(align_items: taffy::style::AlignItems::FlexStart,),
347 "flex-end" => quote!(align_items: taffy::style::AlignItems::FlexEnd,),
348 "center" => quote!(align_items: taffy::style::AlignItems::Center,),
349 "baseline" => quote!(align_items: taffy::style::AlignItems::Baseline,),
350 _ => quote!(),
351 },
352 _ => quote!(),
353 };
354
355 let align_self = match style["alignSelf"] {
356 json::JsonValue::Short(ref value) => match value.as_ref() {
357 "flex-start" => quote!(align_self: taffy::style::AlignSelf::FlexStart,),
358 "flex-end" => quote!(align_self: taffy::style::AlignSelf::FlexEnd,),
359 "center" => quote!(align_self: taffy::style::AlignSelf::Center,),
360 "baseline" => quote!(align_self: taffy::style::AlignSelf::Baseline,),
361 "stretch" => quote!(align_self: taffy::style::AlignSelf::Stretch,),
362 _ => quote!(),
363 },
364 _ => quote!(),
365 };
366
367 let align_content = match style["alignContent"] {
368 json::JsonValue::Short(ref value) => match value.as_ref() {
369 "flex-start" => quote!(align_content: taffy::style::AlignContent::FlexStart,),
370 "flex-end" => quote!(align_content: taffy::style::AlignContent::FlexEnd,),
371 "center" => quote!(align_content: taffy::style::AlignContent::Center,),
372 "space-between" => quote!(align_content: taffy::style::AlignContent::SpaceBetween,),
373 "space-around" => quote!(align_content: taffy::style::AlignContent::SpaceAround,),
374 "space-evenly" => quote!(align_content: taffy::style::AlignContent::SpaceEvenly,),
375 _ => quote!(),
376 },
377 _ => quote!(),
378 };
379
380 let justify_content = match style["justifyContent"] {
381 json::JsonValue::Short(ref value) => match value.as_ref() {
382 "flex-end" => quote!(justify_content: taffy::style::JustifyContent::FlexEnd,),
383 "center" => quote!(justify_content: taffy::style::JustifyContent::Center,),
384 "space-between" => quote!(justify_content: taffy::style::JustifyContent::SpaceBetween,),
385 "space-around" => quote!(justify_content: taffy::style::JustifyContent::SpaceAround,),
386 "space-evenly" => quote!(justify_content: taffy::style::JustifyContent::SpaceEvenly,),
387 _ => quote!(),
388 },
389 _ => quote!(),
390 };
391
392 let flex_grow = quote_number_prop("flex_grow", style, |value: f32| quote!(#value));
393 let flex_shrink = quote_number_prop("flex_shrink", style, |value: f32| quote!(#value));
394
395 let flex_basis = quote_object_prop("flex_basis", style, generate_dimension);
396 let size = quote_object_prop("size", style, generate_size);
397 let min_size = quote_object_prop("min_size", style, generate_size);
398 let max_size = quote_object_prop("max_size", style, generate_size);
399
400 let gap = quote_object_prop("gap", style, generate_gap);
401
402 let grid_template_rows = quote_array_prop("grid_template_rows", style, generate_track_definition_list);
403 let grid_template_columns = quote_array_prop("grid_template_columns", style, generate_track_definition_list);
404 let grid_auto_rows = quote_array_prop("grid_auto_rows", style, generate_track_definition_list);
405 let grid_auto_columns = quote_array_prop("grid_auto_columns", style, generate_track_definition_list);
406 let grid_auto_flow = quote_object_prop("grid_auto_flow", style, generate_grid_auto_flow);
407
408 let grid_row_start = quote_object_prop("grid_row_start", style, generate_grid_position);
409 let grid_row_end = quote_object_prop("grid_row_end", style, generate_grid_position);
410 let grid_column_start = quote_object_prop("grid_column_start", style, generate_grid_position);
411 let grid_column_end = quote_object_prop("grid_column_end", style, generate_grid_position);
412
413 macro_rules! edges_quoted {
414 ($style:ident, $val:ident) => {
415 let $val = match $style[stringify!($val)] {
416 json::JsonValue::Object(ref value) => {
417 let edges = generate_edges(value);
418 quote!($val: #edges,)
419 },
420 _ => quote!(),
421 };
422 };
423 }
424
425 edges_quoted!(style, margin);
426 edges_quoted!(style, padding);
427 edges_quoted!(style, position);
428 edges_quoted!(style, border);
429
430 let (children_body, children) = match node["children"] {
431 json::JsonValue::Array(ref value) => {
432 if !value.is_empty() {
433 let body = value
434 .iter()
435 .enumerate()
436 .map(|(i, child)| generate_node(&format!("{}{}", ident, i), child))
437 .collect();
438 let idents = value
439 .iter()
440 .enumerate()
441 .map(|(i, _)| Ident::new(&format!("{}{}", ident, i), Span::call_site()))
442 .collect::<Vec<_>>();
443 (body, quote!(&[#(#idents),*]))
444 } else {
445 (quote!(), quote!(&[]))
446 }
447 }
448 _ => (quote!(), quote!()),
449 };
450
451 let ident = Ident::new(ident, Span::call_site());
452
453 quote!(
454 #children_body
455 let #ident = taffy.new_with_children(
456 taffy::style::FlexboxLayout {
457 #display
458 #direction
459 #position_type
460 #flex_direction
461 #flex_wrap
462 #overflow
463 #align_items
464 #align_self
465 #align_content
466 #justify_content
467 #flex_grow
468 #flex_shrink
469 #flex_basis
470 #gap
471 #grid_template_rows
472 #grid_template_columns
473 #grid_auto_rows
474 #grid_auto_columns
475 #grid_auto_flow
476 #grid_row_start
477 #grid_row_end
478 #grid_column_start
479 #grid_column_end
480 #size
481 #min_size
482 #max_size
483 #margin
484 #padding
485 #position
486 #border
487 ..Default::default()
488 },
489 #children
490 // TODO: Only add children if they exist
491 ).unwrap();)
492}
493
494macro_rules! dim_quoted_renamed {
495 ($obj:ident, $in_name:ident, $out_name:ident) => {
496 let $out_name = match $obj.get(stringify!($in_name)) {
497 Some(json::JsonValue::Object(ref value)) => {
498 let dim = generate_dimension(value);
499 quote!($out_name: #dim,)
500 }
501 _ => quote!(),
502 };
503 };
504}
505
506macro_rules! dim_quoted {
507 ($obj:ident, $dim_name:ident) => {
508 dim_quoted_renamed!($obj, $dim_name, $dim_name)
509 };
510}
511
512fn generate_size(size: &json::object::Object) -> TokenStream {
513 dim_quoted!(size, width);
514 dim_quoted!(size, height);
515 quote!(
516 taffy::geometry::Size {
517 #width #height
518 ..Default::default()
519 }
520 )
521}
522
523fn generate_gap(size: &json::object::Object) -> TokenStream {
524 dim_quoted_renamed!(size, column, width);
525 dim_quoted_renamed!(size, row, height);
526 quote!(
527 taffy::geometry::Size {
528 #width #height
529 ..Default::default()
530 }
531 )
532}
533
534fn generate_dimension(dimen: &json::object::Object) -> TokenStream {
535 let unit = dimen.get("unit").unwrap();
536 let value = || dimen.get("value").unwrap().as_f32().unwrap();
537
538 match unit {
539 json::JsonValue::Short(ref unit) => match unit.as_ref() {
540 "auto" => quote!(taffy::style::Dimension::Auto),
541 "points" => {
542 let value = value();
543 quote!(taffy::style::Dimension::Points(#value))
544 }
545 "percent" => {
546 let value = value();
547 quote!(taffy::style::Dimension::Percent(#value))
548 }
549 _ => unreachable!(),
550 },
551 _ => unreachable!(),
552 }
553}
554
555fn generate_edges(dimen: &json::object::Object) -> TokenStream {
556 dim_quoted!(dimen, start);
557 dim_quoted!(dimen, end);
558 dim_quoted!(dimen, top);
559 dim_quoted!(dimen, bottom);
560
561 quote!(taffy::geometry::Rect {
562 #start #end #top #bottom
563 ..Default::default()
564 })
565}
566
567fn generate_grid_auto_flow(auto_flow: &json::object::Object) -> TokenStream {
568 let direction = auto_flow.get("direction").unwrap().as_str().unwrap();
569 let algorithm = auto_flow.get("algorithm").unwrap().as_str().unwrap();
570
571 match (direction, algorithm) {
572 ("row", "sparse") => quote!(taffy::style::GridAutoFlow::Row),
573 ("column", "sparse") => quote!(taffy::style::GridAutoFlow::Column),
574 ("row", "dense") => quote!(taffy::style::GridAutoFlow::RowDense),
575 ("column", "dense") => quote!(taffy::style::GridAutoFlow::ColumnDense),
576 _ => unreachable!(),
577 }
578}
579
580fn generate_grid_position(grid_position: &json::object::Object) -> TokenStream {
581 let kind = grid_position.get("kind").unwrap();
582 let value = || grid_position.get("value").unwrap().as_f32().unwrap();
583
584 match kind {
585 json::JsonValue::Short(ref kind) => match kind.as_ref() {
586 "auto" => quote!(taffy::style::GridLine::Auto),
587 "span" => {
588 let value = value() as u16;
589 quote!(taffy::style::GridLine::Span(#value))
590 }
591 "track" => {
592 let value = value() as i16;
593 quote!(taffy::style::GridLine::Track(#value))
594 }
595 _ => unreachable!(),
596 },
597 _ => unreachable!(),
598 }
599}
600
601fn generate_track_definition_list(raw_list: &[json::JsonValue]) -> TokenStream {
602 let list = raw_list.iter().map(|obj| match obj {
603 json::JsonValue::Object(inner) => generate_track_definition(inner),
604 _ => unreachable!(),
605 });
606
607 quote!(vec![#(#list),*])
608}
609
610fn generate_track_definition(track_definition: &json::object::Object) -> TokenStream {
611 let kind = track_definition.get("kind").unwrap().as_str().unwrap();
612 let name = || track_definition.get("name").unwrap().as_str().unwrap();
613 let arguments = || track_definition.get("arguments").unwrap();
614
615 match kind {
616 "scalar" => generate_scalar_definition(track_definition),
617 "function" => match (name(), arguments()) {
618 ("minmax", json::JsonValue::Array(arguments)) => {
619 if arguments.len() != 2 {
620 panic!("minmax function with the wrong number of arguments");
621 }
622 let min = match arguments[0] {
623 json::JsonValue::Object(ref arg) => generate_scalar_definition(arg),
624 _ => unreachable!(),
625 };
626 let max = match arguments[1] {
627 json::JsonValue::Object(ref arg) => generate_scalar_definition(arg),
628 _ => unreachable!(),
629 };
630 quote!(taffy::style::GridTrackSizingFunction::MinMax{ min: #min, max: #max })
631 }
632 // TODO: Add support for fit-content
633 _ => unreachable!(),
634 },
635 _ => unreachable!(),
636 }
637}
638
639fn generate_scalar_definition(track_definition: &json::object::Object) -> TokenStream {
640 let unit = || track_definition.get("unit").unwrap().as_str().unwrap();
641 let value = || track_definition.get("value").unwrap().as_f32().unwrap();
642
643 match unit() {
644 "auto" => quote!(taffy::style::GridTrackSizingFunction::Auto),
645 "min-content" => quote!(taffy::style::GridTrackSizingFunction::MinContent),
646 "max-content" => quote!(taffy::style::GridTrackSizingFunction::MaxContent),
647 "points" | "percent" => {
648 let value = generate_dimension(track_definition);
649 quote!(taffy::style::GridTrackSizingFunction::Fixed(#value))
650 }
651 "fraction" => {
652 let value: f32 = value();
653 quote!(taffy::style::GridTrackSizingFunction::Flex(#value))
654 }
655 _ => unreachable!(),
656 }
657}