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