tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 501HTML 254C 19JavaScript 81412 Definitions

⎇ devin/1784869041-flex-baseline-container tests/xml.rs Rust · 318 lines · 12.4 KB History Who touched what raw

Comine integration tests into one binary (#951) d803b72f9 · Nico Burns ·

1use roxmltree::Document;
2use std::{fmt::Debug, io::Write, path::PathBuf, str::FromStr};
3use taffy::{
4    prelude::TaffyZero as _, AvailableSpace, CheapCloneStr, Dimension, GridAutoTracks, GridTemplateComponent,
5    GridTemplateTracks, LengthPercentage, LengthPercentageAuto, Line, NodeId, Point, PrintTree, Rect, Size, TaffyTree,
6};
7use taffy_test_helpers::{test_measure_function, TestNodeContext};
8
9#[path = "./xml/mod.rs"]
10mod xml;
11
12#[derive(Debug)]
13struct OutputNode {
14    node_id: NodeId,
15    location: Point<f32>,
16    size: Size<f32>,
17    children: Vec<OutputNode>,
18}
19
20impl PartialEq for OutputNode {
21    fn eq(&self, other: &Self) -> bool {
22        self.node_id == other.node_id
23            && (self.location.x - other.location.x).abs() < 0.1
24            && (self.location.y - other.location.y).abs() < 0.1
25            && (self.size.width - other.size.width).abs() < 0.1
26            && (self.size.height - other.size.height).abs() < 0.1
27            && self.children == other.children
28    }
29}
30
31impl OutputNode {
32    fn write_metrics(&self, w: &mut impl Write) {
33        write!(
34            w,
35            "[x: {:<4} y: {:<4} w: {:<4} h: {:<4}]",
36            self.location.x, self.location.y, self.size.width, self.size.height
37        )
38        .unwrap();
39    }
40
41    /// Prints a debug representation of the computed layout for a tree of nodes
42    fn write_tree(&self, out: &mut impl Write) {
43        writeln!(out, "TREE").unwrap();
44        print_node(out, self, false, String::new());
45
46        /// Recursive function that prints each node in the tree
47        fn print_node(out: &mut impl Write, node: &OutputNode, has_sibling: bool, lines_string: String) {
48            let num_children = node.children.len();
49            let fork_string = if has_sibling { "├── " } else { "└── " };
50
51            write!(out, "{lines_string}{fork_string} {:?} ", node.node_id).unwrap();
52            node.write_metrics(out);
53            let bar = if has_sibling { "│   " } else { "    " };
54            let new_string = lines_string + bar;
55            writeln!(out).unwrap();
56
57            // Recurse into children
58            for (index, child) in node.children.iter().enumerate() {
59                let has_sibling = index < num_children - 1;
60                print_node(out, child, has_sibling, new_string.clone());
61            }
62        }
63    }
64
65    fn print_tree(&self) {
66        let mut s: Vec<u8> = Vec::new();
67        self.write_tree(&mut s);
68        let s = String::from_utf8(s).unwrap();
69        println!("{s}");
70    }
71}
72
73#[test]
74fn get_dir() {
75    let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
76    let test_dir = root_dir.join("tests");
77    let xml_test_dir = test_dir.join("xml");
78    println!("{}", xml_test_dir.display())
79}
80
81fn run_xml_test(group: &str, name: &str) {
82    let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
83    let test_dir = root_dir.join("tests");
84    let xml_test_dir = test_dir.join("xml");
85
86    let test_xml_file = {
87        let mut path = xml_test_dir.join(group).join(name);
88        path.set_extension("xml");
89        path
90    };
91
92    let raw_xml = std::fs::read_to_string(&test_xml_file).expect("test file should exist");
93    let test = Document::parse(&raw_xml).unwrap();
94
95    let root = test.root_element();
96    let use_rounding: bool = parse_or(root.attribute("use-rounding"), true);
97    let viewport = root.children().find(|node| node.has_tag_name("viewport")).unwrap();
98    let input = root.children().find(|node| node.has_tag_name("input")).unwrap();
99    let expectations = root.children().find(|node| node.has_tag_name("expectations")).unwrap();
100
101    let available_space = Size {
102        width: parse_or(viewport.attribute("width"), AvailableSpace::MaxContent),
103        height: parse_or(viewport.attribute("height"), AvailableSpace::MaxContent),
104    };
105
106    // Construct tree and expectations
107    let mut tree = TaffyTree::<TestNodeContext>::new();
108    if use_rounding {
109        tree.enable_rounding();
110    } else {
111        tree.disable_rounding();
112    }
113    let expected_output = construct_tree(
114        input.first_element_child().unwrap(),
115        expectations.first_element_child().unwrap(),
116        &mut tree,
117        None,
118    );
119    let root_node_id = expected_output.node_id;
120
121    // Compute layout
122    tree.compute_layout_with_measure(root_node_id, available_space, test_measure_function).unwrap();
123    let actual_output = get_computed_expectations(&tree, root_node_id);
124
125    println!("\nINPUT");
126    println!("{raw_xml}");
127
128    tree.print_tree(root_node_id);
129
130    println!("\nEXPECTED");
131    expected_output.print_tree();
132    println!("\nACTUAL");
133    actual_output.print_tree();
134
135    assert_eq!(expected_output, actual_output);
136}
137
138fn construct_tree(
139    input: roxmltree::Node,
140    expected_x: roxmltree::Node,
141    tree: &mut TaffyTree<TestNodeContext>,
142    parent: Option<taffy::NodeId>,
143) -> OutputNode {
144    if input.first_element_child().is_some() {
145        let tnode = tree.new_with_children(build_style(input), &[]).unwrap();
146        let mut expected = build_expectations(expected_x, tnode);
147
148        if let Some(parent) = parent {
149            tree.add_child(parent, tnode).unwrap();
150        }
151
152        for (child, child_expected_x) in input
153            .children()
154            .filter(|node| node.is_element())
155            .zip(expected_x.children().filter(|node| node.is_element()))
156        {
157            expected.children.push(construct_tree(child, child_expected_x, tree, Some(tnode)));
158        }
159        expected
160    } else {
161        let text_content = input.text().map(|text| text.trim());
162        // let aspect_ratio = input.attribute("aspect-ratio");
163        let writing_mode = parse_or_default(input.attribute("writing-mode"));
164
165        let tnode = tree.new_leaf(build_style(input)).unwrap();
166        tree.set_node_context(
167            tnode,
168            text_content.map(|text_content| TestNodeContext::ahem_text(text_content.to_string(), writing_mode)),
169        )
170        .unwrap();
171
172        if let Some(parent) = parent {
173            tree.add_child(parent, tnode).unwrap();
174        }
175
176        build_expectations(expected_x, tnode)
177    }
178}
179
180fn parse_or_default<T: FromStr<Err: Debug> + Default>(input: Option<&str>) -> T {
181    parse_or(input, Default::default())
182}
183
184fn parse_or<T: FromStr<Err: Debug>>(input: Option<&str>, fallback: T) -> T {
185    input.map(|input| input.parse().unwrap()).unwrap_or(fallback)
186}
187
188fn maybe_parse<T: FromStr>(input: Option<&str>) -> Option<T> {
189    input.and_then(|input| input.parse().ok())
190}
191
192fn get_computed_expectations(tree: &TaffyTree<TestNodeContext>, node_id: NodeId) -> OutputNode {
193    let layout = tree.get_final_layout(node_id);
194    let mut output = OutputNode { node_id, location: layout.location, size: layout.size, children: Vec::new() };
195
196    for child_id in tree.children(node_id).unwrap() {
197        output.children.push(get_computed_expectations(tree, child_id));
198    }
199
200    output
201}
202
203fn build_expectations(xnode: roxmltree::Node, node_id: NodeId) -> OutputNode {
204    OutputNode {
205        node_id,
206        location: Point {
207            x: xnode.attribute("x").unwrap().parse().unwrap(),
208            y: xnode.attribute("y").unwrap().parse().unwrap(),
209        },
210        size: Size {
211            width: xnode.attribute("width").unwrap().parse().unwrap(),
212            height: xnode.attribute("height").unwrap().parse().unwrap(),
213        },
214        children: Vec::new(),
215    }
216}
217
218fn build_style<S: CheapCloneStr>(xnode: roxmltree::Node) -> taffy::Style<S> {
219    let grid_template_rows: GridTemplateTracks<S, GridTemplateComponent<S>> =
220        parse_or_default(xnode.attribute("grid-template-rows"));
221    let grid_template_columns: GridTemplateTracks<S, GridTemplateComponent<S>> =
222        parse_or_default(xnode.attribute("grid-template-columns"));
223
224    taffy::Style {
225        dummy: std::marker::PhantomData,
226        display: parse_or_default(xnode.attribute("display")),
227        direction: parse_or_default(xnode.attribute("direction")),
228        item_is_table: false,
229        item_is_replaced: false,
230        box_sizing: parse_or_default(xnode.attribute("box-sizing")),
231        overflow: Point {
232            x: parse_or_default(xnode.attribute("overflow-x")),
233            y: parse_or_default(xnode.attribute("overflow-y")),
234        },
235        scrollbar_width: parse_or_default(xnode.attribute("scrollbar-width")),
236        float: parse_or_default(xnode.attribute("float")),
237        clear: parse_or_default(xnode.attribute("clear")),
238        position: parse_or_default(xnode.attribute("position")),
239
240        size: Size {
241            width: parse_or(xnode.attribute("width"), Dimension::auto()),
242            height: parse_or(xnode.attribute("height"), Dimension::auto()),
243        },
244        min_size: Size {
245            width: parse_or(xnode.attribute("min-width"), Dimension::auto()),
246            height: parse_or(xnode.attribute("min-height"), Dimension::auto()),
247        },
248        max_size: Size {
249            width: parse_or(xnode.attribute("max-width"), Dimension::auto()),
250            height: parse_or(xnode.attribute("max-height"), Dimension::auto()),
251        },
252        inset: Rect {
253            top: parse_or(xnode.attribute("top"), LengthPercentageAuto::auto()),
254            left: parse_or(xnode.attribute("left"), LengthPercentageAuto::auto()),
255            bottom: parse_or(xnode.attribute("bottom"), LengthPercentageAuto::auto()),
256            right: parse_or(xnode.attribute("right"), LengthPercentageAuto::auto()),
257        },
258        margin: Rect {
259            top: parse_or(xnode.attribute("margin-top"), LengthPercentageAuto::ZERO),
260            left: parse_or(xnode.attribute("margin-left"), LengthPercentageAuto::ZERO),
261            bottom: parse_or(xnode.attribute("margin-bottom"), LengthPercentageAuto::ZERO),
262            right: parse_or(xnode.attribute("margin-right"), LengthPercentageAuto::ZERO),
263        },
264        padding: Rect {
265            top: parse_or(xnode.attribute("padding-top"), LengthPercentage::ZERO),
266            left: parse_or(xnode.attribute("padding-left"), LengthPercentage::ZERO),
267            bottom: parse_or(xnode.attribute("padding-bottom"), LengthPercentage::ZERO),
268            right: parse_or(xnode.attribute("padding-right"), LengthPercentage::ZERO),
269        },
270        border: Rect {
271            top: parse_or(xnode.attribute("border-top"), LengthPercentage::ZERO),
272            left: parse_or(xnode.attribute("border-left"), LengthPercentage::ZERO),
273            bottom: parse_or(xnode.attribute("border-bottom"), LengthPercentage::ZERO),
274            right: parse_or(xnode.attribute("border-right"), LengthPercentage::ZERO),
275        },
276        gap: Size {
277            width: parse_or(xnode.attribute("column-gap"), LengthPercentage::ZERO),
278            height: parse_or(xnode.attribute("row-gap"), LengthPercentage::ZERO),
279        },
280
281        aspect_ratio: maybe_parse(xnode.attribute("aspect-ratio")),
282        align_items: maybe_parse(xnode.attribute("align-items")),
283        align_self: maybe_parse(xnode.attribute("align-self")),
284        justify_items: maybe_parse(xnode.attribute("justify-items")),
285        justify_self: maybe_parse(xnode.attribute("justify-self")),
286        align_content: maybe_parse(xnode.attribute("align-content")),
287        justify_content: maybe_parse(xnode.attribute("justify-content")),
288
289        text_align: parse_or_default(xnode.attribute("text-align")),
290        flex_direction: parse_or_default(xnode.attribute("flex-direction")),
291        flex_wrap: parse_or_default(xnode.attribute("flex-wrap")),
292        flex_grow: parse_or(xnode.attribute("flex-grow"), 0.0),
293        flex_shrink: parse_or(xnode.attribute("flex-shrink"), 1.0),
294        flex_basis: parse_or(xnode.attribute("flex-basis"), Dimension::auto()),
295
296        grid_auto_flow: parse_or_default(xnode.attribute("grid-auto-flow")),
297
298        grid_template_rows: grid_template_rows.tracks,
299        grid_template_row_names: grid_template_rows.line_names,
300
301        grid_template_columns: grid_template_columns.tracks,
302        grid_template_column_names: grid_template_columns.line_names,
303
304        // TODO
305        grid_auto_rows: parse_or_default::<GridAutoTracks>(xnode.attribute("grid-auto-rows")).0,
306        grid_auto_columns: parse_or_default::<GridAutoTracks>(xnode.attribute("grid-auto-columns")).0,
307        grid_template_areas: Default::default(),
308
309        grid_row: Line {
310            start: parse_or_default(xnode.attribute("grid-row-start")),
311            end: parse_or_default(xnode.attribute("grid-row-end")),
312        },
313        grid_column: Line {
314            start: parse_or_default(xnode.attribute("grid-column-start")),
315            end: parse_or_default(xnode.attribute("grid-column-end")),
316        },
317    }
318}