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