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