1use crate::yui::{Element, RAW_CALL, err};
29use yog_ids::{yui, yui_attr as at};
30
31const ATTACHED: &[&str] = &[
32 at::GRID_ROW,
33 at::GRID_COLUMN,
34 at::GRID_ROWSPAN,
35 at::GRID_COLUMNSPAN,
36];
37
38fn num(v: &str) -> Option<f32> {
39 v.trim()
40 .parse::<f32>()
41 .ok()
42 .filter(|n| n.is_finite() && *n >= 0.0)
43}
44
45fn length(v: &str) -> Option<String> {
47 let v = v.trim();
48 if v.eq_ignore_ascii_case("auto") {
49 return Some("::yog_api::ui::Track::auto()".into());
50 }
51 if let Some(w) = v.strip_suffix('*') {
52 let w = if w.trim().is_empty() {
53 Some(1.0)
54 } else {
55 num(w)
56 }?;
57 return Some(format!("::yog_api::ui::Track::star({w:?})"));
58 }
59 num(v).map(|px| format!("::yog_api::ui::Track::px({px:?})"))
60}
61
62fn compact(el: &Element, key: &str, value: &str, what: &str) -> Result<Vec<String>, String> {
64 if value.trim_start().starts_with('{') {
65 return err(
66 el,
67 format_args!("{key}=\"{value}\": grid definitions can't be bound"),
68 );
69 }
70 value
71 .split(',')
72 .map(|part| {
73 length(part).ok_or_else(|| {
74 format!(
75 "line {}: <{}> {key}=\"{value}\": `{}` isn't a {what} size — expected Auto, *, 2* or pixels (26)",
76 el.line,
77 el.name,
78 part.trim()
79 )
80 })
81 })
82 .collect()
83}
84
85fn elements(prop: &Element, row: bool) -> Result<Vec<String>, String> {
87 let (def, size, min, max) = if row {
88 (
89 yui::ROW_DEFINITION,
90 at::HEIGHT,
91 at::MIN_HEIGHT,
92 at::MAX_HEIGHT,
93 )
94 } else {
95 (
96 yui::COLUMN_DEFINITION,
97 at::WIDTH,
98 at::MIN_WIDTH,
99 at::MAX_WIDTH,
100 )
101 };
102 if !prop.attrs.is_empty() {
103 return err(prop, "takes no attributes");
104 }
105 let mut out = Vec::new();
106 for d in &prop.children {
107 if d.name != def {
108 return err(
109 d,
110 format_args!("isn't allowed in <{}> (expected <{def}>)", prop.name),
111 );
112 }
113 if !d.children.is_empty() {
114 return err(d, "can't have children");
115 }
116 for (k, _) in &d.attrs {
117 if ![size, min, max].contains(&k.as_str()) {
118 let lower = k
119 .chars()
120 .next()
121 .is_some_and(char::is_uppercase)
122 .then(|| crate::yui::snake(k));
123 return match lower.filter(|l| [size, min, max].contains(&l.as_str())) {
124 Some(l) => err(
125 d,
126 format_args!("has no attribute `{k}` — .yui attributes are lowercase: {l}"),
127 ),
128 None => err(
129 d,
130 format_args!("has no attribute `{k}` (expected {size}, {min}, {max})"),
131 ),
132 };
133 }
134 }
135 let v = d.attr(size).unwrap_or("*");
136 let mut t = length(v).ok_or_else(|| {
137 format!(
138 "line {}: <{def}> {size}=\"{v}\": expected Auto, *, 2* or pixels (26)",
139 d.line
140 )
141 })?;
142 for (key, call) in [(min, "min"), (max, "max")] {
143 if let Some(v) = d.attr(key) {
144 let n = num(v).ok_or_else(|| {
145 format!(
146 "line {}: <{def}> {key}=\"{v}\": expected a number of pixels",
147 d.line
148 )
149 })?;
150 t.push_str(&format!(".{call}({n:?})"));
151 }
152 }
153 out.push(t);
154 }
155 Ok(out)
156}
157
158fn index(el: &Element, key: &str, v: &str, min: u16) -> Result<u16, String> {
159 v.trim()
160 .parse::<u16>()
161 .ok()
162 .filter(|n| *n >= min)
163 .ok_or_else(|| {
164 let what = if min == 0 {
165 "a whole number (0 = the first)"
166 } else {
167 "a whole number, at least 1"
168 };
169 format!(
170 "line {}: <{}> {key}=\"{v}\": expected {what}",
171 el.line, el.name
172 )
173 })
174}
175
176fn plural(n: usize, what: &str) -> String {
177 if n == 1 {
178 format!("1 {what}")
179 } else {
180 format!("{n} {what}s")
181 }
182}
183
184#[derive(Default)]
186struct Tracks {
187 rows: Option<Vec<String>>,
188 cols: Option<Vec<String>>,
189}
190
191impl Tracks {
192 fn slot(&mut self, row: bool) -> &mut Option<Vec<String>> {
193 if row { &mut self.rows } else { &mut self.cols }
194 }
195}
196
197const BINDING_START: char = '{';
199const GRID_MEMBER: &str = yog_ids::concatcp!(yui::GRID, yog_ids::sep::MEMBER);
201
202pub fn normalize(el: &mut Element) -> Result<(), String> {
205 let is_grid = el.name == yui::GRID;
206 let mut tracks = definitions(el, is_grid)?;
207 if is_grid {
208 let calls = grid_calls(el, &mut tracks)?;
209 push_calls(el, calls, "g");
210 }
211 let size = (
212 tracks.rows.as_ref().map_or(1, Vec::len),
213 tracks.cols.as_ref().map_or(1, Vec::len),
214 );
215 let name = el.name.clone();
216 for child in &mut el.children {
217 cell(child, is_grid.then_some(size), &name)?;
218 }
219 for child in &mut el.children {
220 normalize(child)?;
221 }
222 Ok(())
223}
224
225fn definitions(el: &mut Element, is_grid: bool) -> Result<Tracks, String> {
228 let mut tracks = Tracks::default();
229 let mut kept = Vec::with_capacity(el.children.len());
230 for child in std::mem::take(&mut el.children) {
231 let row = match child.name.as_str() {
232 yui::ROW_DEFINITIONS => true,
233 yui::COLUMN_DEFINITIONS => false,
234 n if n.starts_with(GRID_MEMBER) => {
235 return err(
236 &child,
237 "isn't a Grid property (expected <Grid.RowDefinitions> or <Grid.ColumnDefinitions>)",
238 );
239 }
240 _ => {
241 kept.push(child);
242 continue;
243 }
244 };
245 if !is_grid {
246 return err(
247 &child,
248 format_args!("only goes directly inside a <Grid>, not <{}>", el.name),
249 );
250 }
251 if tracks.slot(row).is_some() {
252 return err(&child, "is given twice");
253 }
254 *tracks.slot(row) = Some(elements(&child, row)?);
255 }
256 el.children = kept;
257 Ok(tracks)
258}
259
260fn grid_calls(el: &mut Element, tracks: &mut Tracks) -> Result<Vec<String>, String> {
262 let mut calls = Vec::new();
263 let mut attrs = Vec::new();
264 for (k, v) in std::mem::take(&mut el.attrs) {
265 match k.as_str() {
266 at::ROWS | at::COLUMNS => {
267 let row = k == at::ROWS;
268 if tracks.slot(row).is_some() {
269 let prop = if row {
270 yui::ROW_DEFINITIONS
271 } else {
272 yui::COLUMN_DEFINITIONS
273 };
274 return err(
275 el,
276 format_args!("{k}=\"...\" and <{prop}> both define the {k} — keep one"),
277 );
278 }
279 *tracks.slot(row) = Some(compact(el, &k, &v, if row { "row" } else { "column" })?);
280 }
281 at::ROW_SPACING | at::COLUMN_SPACING => {
282 let n = num(&v).ok_or_else(|| {
283 format!(
284 "line {}: <Grid> {k}=\"{v}\": expected a number of pixels",
285 el.line
286 )
287 })?;
288 let m = if k == at::ROW_SPACING {
289 "row_spacing"
290 } else {
291 "col_spacing"
292 };
293 calls.push(format!("{m}({n:?})"));
294 }
295 _ => attrs.push((k, v)),
296 }
297 }
298 el.attrs = attrs;
299 if let Some(r) = &tracks.rows {
300 calls.push(format!("rows([{}])", r.join(", ")));
301 }
302 if let Some(c) = &tracks.cols {
303 calls.push(format!("cols([{}])", c.join(", ")));
304 }
305 Ok(calls)
306}
307
308fn cell(child: &mut Element, grid: Option<(usize, usize)>, parent: &str) -> Result<(), String> {
311 let mut cell = [0u16, 0, 1, 1];
312 let mut any = false;
313 let mut attrs = Vec::new();
314 for (k, v) in std::mem::take(&mut child.attrs) {
315 let Some(i) = ATTACHED.iter().position(|a| *a == k) else {
316 if k.starts_with(GRID_MEMBER) {
317 return err(
318 child,
319 format_args!("has no attribute `{k}` (attached: {})", ATTACHED.join(", ")),
320 );
321 }
322 attrs.push((k, v));
323 continue;
324 };
325 if grid.is_none() {
326 return err(
327 child,
328 format_args!(
329 "{k}: only children of a <Grid> have grid cells (this is in a <{parent}>)"
330 ),
331 );
332 }
333 if v.trim_start().starts_with(BINDING_START) {
334 return err(
335 child,
336 format_args!("{k}=\"{v}\": grid cells can't be bound"),
337 );
338 }
339 cell[i] = index(child, &k, &v, if i < 2 { 0 } else { 1 })?;
340 any = true;
341 }
342 child.attrs = attrs;
343 let Some((n_rows, n_cols)) = grid else {
344 return Ok(());
345 };
346 let [r, c, rs, cs] = cell;
347 for (start, span, n, what, key) in [
348 (r, rs, n_rows, "row", at::GRID_ROW),
349 (c, cs, n_cols, "column", at::GRID_COLUMN),
350 ] {
351 let (start, span) = (start as usize, span as usize);
352 if start >= n {
353 return err(
354 child,
355 format_args!(
356 "{key}=\"{start}\" is past the grid's last {what} (it has {}, numbered from 0)",
357 plural(n, what)
358 ),
359 );
360 }
361 if start + span > n {
362 return err(
363 child,
364 format_args!(
365 "{key}Span=\"{span}\" from {what} {start} runs past the grid's last {what} (it has {})",
366 plural(n, what)
367 ),
368 );
369 }
370 }
371 if any {
372 let mut calls = vec![format!("cell({r}, {c})")];
373 if (rs, cs) != (1, 1) {
374 calls.push(format!("span({rs}, {cs})"));
375 }
376 push_calls(child, calls, "c");
377 }
378 Ok(())
379}
380
381fn push_calls(el: &mut Element, calls: Vec<String>, tag: &str) {
383 for (i, c) in calls.into_iter().enumerate() {
384 el.attrs.push((format!("{RAW_CALL}{tag}{i}"), c));
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use crate::yui;
391
392 fn codegen(body: &str) -> Result<String, String> {
393 yui::generate(
394 &format!("<Screen id=\"m:s\" width=\"100\" height=\"100\">{body}</Screen>"),
395 "s",
396 )
397 }
398
399 #[test]
400 fn compact_and_element_forms() {
401 let code = codegen(
402 r#"<Grid rows="Auto, *, 26" row_spacing="2" column_spacing="4">
403 <Grid.ColumnDefinitions>
404 <ColumnDefinition width="2*" min_width="80"/>
405 <ColumnDefinition max_width="200"/>
406 </Grid.ColumnDefinitions>
407 <Label text="a" Grid.Row="1" Grid.Column="1" halign="right" margin="2 4"/>
408 <Label text="b" Grid.RowSpan="3"/>
409 <Label text="c"/>
410 </Grid>"#,
411 )
412 .unwrap();
413 assert!(code.contains("widget::grid()"), "{code}");
414 assert!(code.contains(".row_spacing(2.0)"), "{code}");
415 assert!(code.contains(".col_spacing(4.0)"), "{code}");
416 assert!(
417 code.contains(".rows([::yog_api::ui::Track::auto(), ::yog_api::ui::Track::star(1.0), ::yog_api::ui::Track::px(26.0)])"),
418 "{code}"
419 );
420 assert!(
421 code.contains(".cols([::yog_api::ui::Track::star(2.0).min(80.0), ::yog_api::ui::Track::star(1.0).max(200.0)])"),
422 "{code}"
423 );
424 assert!(code.contains(".cell(1, 1)"), "{code}");
425 assert!(
426 code.contains(".halign(::yog_api::ui::HAlign::Right)"),
427 "{code}"
428 );
429 assert!(code.contains(".margin(2.0, 4.0, 2.0, 4.0)"), "{code}");
430 assert!(
431 code.contains(".cell(0, 0)\n") && code.contains(".span(3, 1)"),
432 "{code}"
433 );
434 }
435
436 #[test]
437 fn errors() {
438 let e = |body: &str| codegen(body).unwrap_err();
439 assert!(e(r#"<Grid rows="abc"/>"#).contains("`abc` isn't a row size"));
440 assert!(e(r#"<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/></Grid.RowDefinitions></Grid>"#)
441 .contains("lowercase: height"));
442 assert!(e(
443 r#"<Grid><Grid.RowDefinitions><RowDefinition height="x"/></Grid.RowDefinitions></Grid>"#
444 )
445 .contains("height=\"x\""));
446 assert!(
447 e(r#"<Grid rows="*,*"><Label Grid.Row="2"/></Grid>"#)
448 .contains("past the grid's last row (it has 2 rows")
449 );
450 assert!(
451 e(r#"<Grid columns="*,*"><Label Grid.Column="1" Grid.ColumnSpan="2"/></Grid>"#)
452 .contains("Grid.ColumnSpan=\"2\" from column 1")
453 );
454 assert!(e(r#"<Grid><Label Grid.Row="1"/></Grid>"#).contains("it has 1 row,"));
455 assert!(
456 e(r#"<Column><Label Grid.Row="0"/></Column>"#).contains("only children of a <Grid>")
457 );
458 assert!(e(r#"<Grid rows="*"><Grid.RowDefinitions/></Grid>"#).contains("keep one"));
459 assert!(
460 e(r#"<Column><Grid.RowDefinitions/></Column>"#).contains("directly inside a <Grid>")
461 );
462 assert!(e(r#"<Grid><Label Grid.RowSpan="0"/></Grid>"#).contains("at least 1"));
463 assert!(e(r#"<Grid><Label HorizontalAlignment="Left"/></Grid>"#).contains("halign"));
464 assert!(
465 e(r#"<Grid><Label halign="middle"/></Grid>"#).contains("left, center, right, stretch")
466 );
467 }
468}